30 Commits
v3.0.1 ... v3.1

Author SHA1 Message Date
ShirkNeko
bb2d8fd7e0 Refactoring the KsuIsValid Check Logic
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
2025-05-12 00:06:17 +08:00
ShirkNeko
2b6d418fe6 [skip ci]:Removing unused LocalDensity variables 2025-05-11 23:36:09 +08:00
ShirkNeko
d8b1126b96 [skip ci]:Modify comments 2025-05-11 23:32:46 +08:00
ShirkNeko
2eeddcfa80 Modify UI layout
- Adjust the maximum number of SwitchItem rows to optimize the layout and spacing of interface buttons
2025-05-11 23:30:48 +08:00
ShirkNeko
59e3675a36 {docs}:Fixed description of KPROBES and manual hooks, simplified content 2025-05-10 12:17:00 +08:00
米凛MiRin
bc386f080d 修正 README 中错误和误导性内容。 (#71)
* 修正文档

* Update README-en.md

* Update README-ja.md
2025-05-09 22:27:53 +08:00
ShirkNeko
2dc1377154 Update Android Gradle plugin version to 8.10.0 2025-05-09 19:14:46 +08:00
WenHao2130
610852e2f2 [skip ci]: manager: modify background image control logic (#70)
* manager: modify background image control logic

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

* manager: modify padding

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

* docs: update README.md README-en.md README-ja.md

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

---------

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-05-09 16:58:01 +08:00
ShirkNeko
15b19bb8ce Remove unnecessary card color calculations and simplify theme colors 2025-05-08 11:58:28 +08:00
ShirkNeko
4a598b1837 [skip ci]: Correction of translation errors 2025-05-07 11:30:01 +08:00
ShirkNeko
caee2417d6 [skip ci]:
Fixing tools used by kernels under 5.10
-Add Slot selection is not displayed for non-ab partitions
2025-05-05 22:09:01 +08:00
ShirkNeko
349ca36d4e [skip ci]: Remove unnecessary center point calculation code to simplify bitmap transformation logic 2025-05-05 21:09:31 +08:00
ShirkNeko
ec86f5caf2 [skip ci]:Simplifying Conditional Judgment in the Selection of Installation Methods 2025-05-05 21:09:31 +08:00
ShirkNeko
b5a5cdfcd2 [skip ci]: Fixed “Kernel Module” to “KPM” in string resources. 2025-05-05 21:09:31 +08:00
YC酱luyancib
72d799e065 [skip]: manager: adjust translate on zh-rCN 2025-05-05 21:09:30 +08:00
ShirkNeko
d06f22dcd0 manager: continue to improve the UI
- Expose anykernel3 flashing as long as there is root.
- Opt some styles
2025-05-05 21:09:30 +08:00
ShirkNeko
cb90630f27 Optimize the interface, add hidden link card function, adjust scrolling behavior, clean up unnecessary code 2025-05-05 21:09:30 +08:00
Re*Index. (ot_inc)
59ad9204d0 Update Japanese translated (#64) 2025-05-05 21:09:30 +08:00
ShirkNeko
cb97c16f5e Fix LKM build error due to kernel module listing
Co-authored-by: James McConnell <bins4us@hotmail.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-05 21:04:25 +08:00
ShirkNeko
69b48d5345 Comment out the cleanup command to avoid accidentally deleting protected exports. 2025-04-30 20:27:54 +08:00
ShirkNeko
45ed4708c9 Optimize the HomeScreen component, refactor the device model acquisition logic, add anti-shake scrolling processing, clean up unused imports 2025-04-30 20:01:39 +08:00
ShirkNeko
f3c77bdb3b [skip ci]: Remove unused animation imports to optimize code cleanliness 2025-04-30 19:49:59 +08:00
ShirkNeko
dc0eb9eec1 Fix duplicate creation of popup windows 2025-04-30 19:48:40 +08:00
ShirkNeko
83dd6443cb Optimize KpmScreen interface layout, adjust button and text display, update signature configuration code 2025-04-30 02:49:09 +08:00
ShirkNeko
3d77f2d135 Adjust the spacing and size of interface elements to optimize the layout effect 2025-04-29 21:46:38 +08:00
ShirkNeko
1ea219bddc Updated GKI installation selection style 2025-04-29 18:07:29 +08:00
ShirkNeko
39adba62d1 Update the default theme color to blue and remove the related blue theme code 2025-04-29 17:29:45 +08:00
ShirkNeko
3526e84e04 Refactor the UI to rewrite the interface (#61) 2025-04-29 15:52:56 +08:00
ShirkNeko
bfdb706b60 Add kernel version and patch tool version log information
- Should fix the 5.10 bug where you can't swipe write

Signed-off-by: ShirkNeko 109797057+ShirkNeko@users.noreply.github.com
2025-04-28 16:05:59 +08:00
ShirkNeko
a297e07055 Adjust the prompt for file selection and add instructions for mirror repair.
- Modify the maximum height of the progress bar to improve user experience
- Add localized strings for error messages and installation methods.
-Optimize the installation interface

Signed-off-by: ShirkNeko 109797057+ShirkNeko@users.noreply.github.com
2025-04-28 14:39:09 +08:00
37 changed files with 4398 additions and 2130 deletions

View File

@@ -198,6 +198,9 @@ jobs:
- name: Make working directory clean to avoid dirty - name: Make working directory clean to avoid dirty
working-directory: android-kernel working-directory: android-kernel
run: | run: |
if [ -e common/BUILD.bazel ]; then
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
fi
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!" rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
git config --global user.email "bot@kernelsu.org" git config --global user.email "bot@kernelsu.org"
git config --global user.name "KernelSUBot" git config --global user.name "KernelSUBot"

View File

@@ -2,22 +2,19 @@
**English** | [简体中文](README.md) | [日本語](README-ja.md) **English** | [简体中文](README.md) | [日本語](README-ja.md)
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU) Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental! **Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
>
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann) > This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
> However, we will be a separately maintained branch of KSU in the future
> >
> However, we will be a separately maintained branch of KSU in the future
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only) - Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
## How to add ## How to add
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices) Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
``` ```
@@ -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 1. Use the susfs-dev branch directly without any patching
## KPM support ## KPM support
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support. - We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality. - We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM template address: https://github.com/udochina/KPM-Build-Anywhere KPM template address: https://github.com/udochina/KPM-Build-Anywhere
## More links ## More links
Projects compiled based on Sukisu and susfs Projects compiled based on Sukisu and susfs
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) - [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
@@ -53,32 +47,34 @@ Projects compiled based on Sukisu and susfs
- This method references the hook method from (https://github.com/rsuntk/KernelSU) - This method references the hook method from (https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:** 1. **KPROBES hook:**
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks. - Also used for Loadable Kernel Module (LKM)
- For Loadable Kernel Modules (LKM) - Default hook method on GKI kernels.
- Default hooking method for GKI kernels - Need `CONFIG_KPROBES=y`
- Requires `CONFIG_KPROBES=y`.
2. **Manual hooks:**
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
2. **Manual hook:**
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
- Default hook method on Non-GKI kernels.
- Need `CONFIG_KSU_MANUAL_HOOK=y`
## Usage ## Usage
### GKI ### 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 Please follow this guide.
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.
https://kernelsu.org/guide/installation.html
### OnePlus ### OnePlus
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix. 1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
> [!Note] > [!Note]
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6. > - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
> - Please search for the processor codename by yourself, usually it is all English without numbers. > - Please search for the processor codename by yourself, usually it is all English without numbers.
> - You can find the branch and configuration files from the OnePlus open-source kernel repository. > - You can find the branch and configuration files from the OnePlus open-source kernel repository.
## Features ## Features
1. Kernel-based `su` and root access management. 1. Kernel-based `su` and root access management.
@@ -88,23 +84,19 @@ Projects compiled based on Sukisu and susfs
5. More customization 5. More customization
6. Support for KPM kernel modules 6. Support for KPM kernel modules
## License ## License
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license. - The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license. - All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
## Sponsorship list ## Sponsorship list
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support - [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea - [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project - [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks - [yspbwx2010](https://github.com/yspbwx2010) Many thanks
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao - [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support! If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
## Contributions ## Contributions

View File

@@ -7,16 +7,15 @@
**試験中なビルドです!自己責任で使用してください!**<br> **試験中なビルドです!自己責任で使用してください!**<br>
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。 このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
>
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。 > これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
> >
>ただし、将来的には KSU とは別に管理されるブランチとなる予定です。 > ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ) - 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 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 curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
``` ```
## 統合された susfs の使い方 ## 統合された susfs の使い方
1. パッチを当てずに susfs-dev ブランチを直接使用してください。 1. パッチを当てずに susfs-dev ブランチを直接使用してください。
## KPM に対応 ## KPM に対応
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。 - KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。 - KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
## その他のリンク ## その他のリンク
SukiSU と susfs をベースにコンパイルされたプロジェクトです。 SukiSU と susfs をベースにコンパイルされたプロジェクトです。
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) - [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## フックの方式 ## フックの方式
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。 - この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
1. **KPROBES フック:** 1. **KPROBES フック:**
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
- 読み込み可能なカーネルモジュールの場合 (LKM) - 読み込み可能なカーネルモジュールの場合 (LKM)
- GKI カーネルのデフォルトとなるフック方式 - GKI カーネルのデフォルトとなるフック方式
- `CONFIG_KPROBES=y` が必要です - `CONFIG_KPROBES=y` が必要です
2. **手動でフック:** 2. **手動でフック:**
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5 - backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。 - 非 GKI カーネル用のデフォルトフッキングメソッド
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
## 使い方 ## 使い方
### GKI ### 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 ### OnePlus
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。 1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
> [!Note] > [!Note]
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。 > - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。 > - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。 > - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
## 機能 ## 機能
1. カーネルベースな `su` および root アクセスの管理。 1. カーネルベースな `su` および root アクセスの管理。
@@ -81,23 +85,19 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
5. その他のカスタマイズ 5. その他のカスタマイズ
6. KPM カーネルモジュールに対応 6. KPM カーネルモジュールに対応
## ライセンス ## ライセンス
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。 - “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。 - “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
## スポンサーシップの一覧 ## スポンサーシップの一覧
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。 - [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。 - [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。 - [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。 - [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう - [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。 上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
## 貢献者 ## 貢献者

View File

@@ -6,23 +6,19 @@
**实验性! 使用风险自负!** **实验性! 使用风险自负!**
>
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann) > 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
> 但是我们将会在未来成为一个单独维护的KSU分支
> >
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
## 如何添加 ## 如何添加
在内核源码的根目录下执行以下命令: 在内核源码的根目录下执行以下命令:
使用 susfs-dev 分支已集成susfs带非GKI设备的支持 使用 susfs-dev 分支(已集成 susfs带非 GKI 设备的支持)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
``` ```
使用 main 分支 使用 main 分支
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
@@ -33,46 +29,49 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs 1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
## 钩子方法 ## 钩子方法
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU) - 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
1. **KPROBES 钩子:** 1. **KPROBES 钩子:**
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
- 用于可加载内核模块 (LKM) - 用于可加载内核模块 (LKM)
- GKI 2.0 内核的默认钩子方法 - GKI 2.0 内核的默认钩子方法
- 需要 `CONFIG_KPROBES=y` - 需要 `CONFIG_KPROBES=y`
2. **手动钩子:** 2. **手动钩子:**
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
- 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5 - backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁 - 非 GKI 内核的默认挂钩方法
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
## KPM 支持
## KPM支持 - 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
- 我们基于KernelPatch去掉了和KSU重复的功能保留了KPM支持
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch 开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
## 更多链接 ## 更多链接
基于 SukiSU 和 susfs 编译的项目 基于 SukiSU 和 susfs 编译的项目
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) - [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## 使用方法 ## 使用方法
### GKI ### 普适的 GKI
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可 请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
3. 一般不带后缀的 .zip 压缩包是通用gz 后缀的为天玑机型专用lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
> [!Note]
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
> 3. 一般不带后缀的 .zip 压缩包是未压缩的gz 后缀的为天玑机型所使用的压缩方式
### 一加 ### 一加
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可 1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
> [!Note] > [!Note]
@@ -80,7 +79,6 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
> - 处理器代号请自行搜索,一般为全英文不带数字的代号 > - 处理器代号请自行搜索,一般为全英文不带数字的代号
> - 分支和配置文件请自行到一加内核开源地址进行填写 > - 分支和配置文件请自行到一加内核开源地址进行填写
## 特点 ## 特点
1. 基于内核的 `su` 和 root 访问管理 1. 基于内核的 `su` 和 root 访问管理
@@ -88,8 +86,7 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里 3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
4. 恢复对非 GKI 2.0 内核的支持 4. 恢复对非 GKI 2.0 内核的支持
5. 更多自定义功能 5. 更多自定义功能
6. 对KPM内核模块的支持 6. KPM 内核模块的支持
## 许可证 ## 许可证
@@ -97,15 +94,13 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
-`kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。 -`kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
## 赞助名单 ## 赞助名单
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持 - [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错 - [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持 - [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢 - [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT - [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持 如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
## 贡献 ## 贡献
@@ -118,4 +113,4 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具 - [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
- [genuine](https://github.com/brevent/genuine/)APK v2 签名验证 - [genuine](https://github.com/brevent/genuine/)APK v2 签名验证
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能 - [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatchAPatch实现内核模块的关键部分 - [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatchAPatch 实现内核模块的关键部分

View File

@@ -1,5 +1,6 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ApkSigningConfig
import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.gradle.tasks.PackageAndroidArtifact import com.android.build.gradle.tasks.PackageAndroidArtifact
@@ -24,7 +25,17 @@ apksign {
keyPasswordProperty = "KEY_PASSWORD" keyPasswordProperty = "KEY_PASSWORD"
} }
android { android {
/**signingConfigs {
create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = ""
keyAlias = ""
keyPassword = ""
}
}**/
namespace = "com.sukisu.ultra" namespace = "com.sukisu.ultra"
buildTypes { buildTypes {
@@ -33,6 +44,9 @@ android {
isShrinkResources = true isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
/**debug {
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
}**/
} }
buildFeatures { buildFeatures {

View File

@@ -3,8 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application <application

Binary file not shown.

View File

@@ -91,14 +91,6 @@ object Natives {
return version < MINIMAL_SUPPORTED_KERNEL return version < MINIMAL_SUPPORTED_KERNEL
} }
fun isKsuValid(pkgName: String?): Boolean {
if (becomeManager(pkgName)) {
return true
} else {
return false
}
}
@Immutable @Immutable
@Parcelize @Parcelize
@Keep @Keep

View File

@@ -133,15 +133,13 @@ class HorizonKernelWorker(
state.updateStep(context.getString(R.string.horizon_flashing)) state.updateStep(context.getString(R.string.horizon_flashing))
state.updateProgress(0.7f) state.updateProgress(0.7f)
// 获取原始槽位信息 val isAbDevice = isAbDevice()
if (slot != null) {
if (isAbDevice && slot != null) {
state.updateStep(context.getString(R.string.horizon_getting_original_slot)) state.updateStep(context.getString(R.string.horizon_getting_original_slot))
state.updateProgress(0.72f) state.updateProgress(0.72f)
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix") originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
}
// 设置目标槽位
if (!slot.isNullOrEmpty()) {
state.updateStep(context.getString(R.string.horizon_setting_target_slot)) state.updateStep(context.getString(R.string.horizon_setting_target_slot))
state.updateProgress(0.74f) state.updateProgress(0.74f)
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot") runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
@@ -149,8 +147,7 @@ class HorizonKernelWorker(
flash() flash()
// 恢复原始槽位 if (isAbDevice && !originalSlot.isNullOrEmpty()) {
if (!originalSlot.isNullOrEmpty()) {
state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
state.updateProgress(0.8f) state.updateProgress(0.8f)
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
@@ -165,8 +162,7 @@ class HorizonKernelWorker(
} catch (e: Exception) { } catch (e: Exception) {
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error)) state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
// 恢复原始槽位 if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
if (!originalSlot.isNullOrEmpty()) {
state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
state.updateProgress(0.8f) state.updateProgress(0.8f)
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
@@ -174,6 +170,17 @@ class HorizonKernelWorker(
} }
} }
// 检查设备是否为AB分区设备
private fun isAbDevice(): Boolean {
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
return false
}
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
return !slotSuffix.isNullOrEmpty()
}
private fun cleanup() { private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete") runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
} }
@@ -196,9 +203,25 @@ class HorizonKernelWorker(
} }
private fun patch() { private fun patch() {
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs" val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath) val versionRegex = """\d+\.\d+\.\d+""".toRegex()
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath") val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
val toolName = if (version.isNotEmpty()) {
val parts = version.split('.')
if (parts.size >= 2) {
val major = parts[0].toIntOrNull() ?: 0
val minor = parts[1].toIntOrNull() ?: 0
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
} else {
"5_15+"
}
} else {
"5_15+"
}
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
} }
private fun flash() { private fun flash() {
@@ -275,7 +298,7 @@ class HorizonKernelWorker(
} }
} }
private fun runCommandGetOutput(su: Boolean, cmd: String): String { private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
val process = ProcessBuilder(if (su) "su" else "sh") val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true) .redirectErrorStream(true)
.start() .start()
@@ -354,7 +377,7 @@ fun HorizonKernelFlashProgress(state: FlashState) {
Surface( Surface(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 150.dp) .heightIn(max = 230.dp)
.padding(vertical = 4.dp), .padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp, tonalElevation = 1.dp,

View File

@@ -1,19 +1,20 @@
package com.sukisu.ultra.ui package com.sukisu.ultra.ui
import android.database.ContentObserver
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@@ -29,24 +30,68 @@ import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.screen.BottomBarDestination import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.* import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
class MainActivity : ComponentActivity() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge // Enable edge to edge
enableEdgeToEdge() enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false window.isNavigationBarContrastEnforced = false
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
if (isFirstRun) {
ThemeConfig.preventBackgroundRefresh = false
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
prefs.edit { putBoolean("is_first_run", false) }
}
// 加载保存的背景设置 // 加载保存的背景设置
loadCustomBackground()
loadThemeMode() loadThemeMode()
loadThemeColors()
loadDynamicColorState()
CardConfig.load(applicationContext) CardConfig.load(applicationContext)
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
runOnUiThread {
if (!ThemeConfig.preventBackgroundRefresh) {
ThemeConfig.backgroundImageLoaded = false
loadCustomBackground()
}
}
}
contentResolver.registerContentObserver(
android.provider.Settings.System.getUriFor("ui_night_mode"),
false,
contentObserver
)
val destroyListeners = mutableListOf<() -> Unit>()
destroyListeners.add {
contentResolver.unregisterContentObserver(contentObserver)
}
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = Natives.becomeManager(ksuApp.packageName)
if (isManager) { if (isManager) {
@@ -58,12 +103,13 @@ class MainActivity : ComponentActivity() {
KernelSUTheme { KernelSUTheme {
val navController = rememberNavController() val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() } val snackBarHostState = remember { SnackbarHostState() }
Scaffold( Scaffold(
bottomBar = { BottomBar(navController) }, bottomBar = { BottomBar(navController) },
contentWindowInsets = WindowInsets(0, 0, 0, 0) contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding -> ) { innerPadding ->
CompositionLocalProvider( CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState, LocalSnackbarHost provides snackBarHostState
) { ) {
DestinationsNavHost( DestinationsNavHost(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
@@ -81,26 +127,50 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
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 @Composable
private fun BottomBar(navController: NavHostController) { private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator() val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
val containerColor = MaterialTheme.colorScheme.surfaceVariant
// 获取卡片颜色和透明度 val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardColor = MaterialTheme.colorScheme.secondaryContainer
val cardAlpha = CardConfig.cardAlpha
val cardElevation = CardConfig.cardElevation
NavigationBar( NavigationBar(
tonalElevation = cardElevation, // 动态设置阴影 modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
),
containerColor = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha), containerColor = cardColor.copy(alpha = cardAlpha),
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom ).containerColor,
) tonalElevation = cardElevation
) { ) {
BottomBarDestination.entries.forEach { destination -> BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) { if (destination == BottomBarDestination.Kpm) {
@@ -110,9 +180,7 @@ private fun BottomBar(navController: NavHostController) {
NavigationBarItem( NavigationBarItem(
selected = isCurrentDestOnBackStack, selected = isCurrentDestOnBackStack,
onClick = { onClick = {
if (isCurrentDestOnBackStack) { if (!isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) { navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) { popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true saveState = true
@@ -120,19 +188,25 @@ private fun BottomBar(navController: NavHostController) {
launchSingleTop = true launchSingleTop = true
restoreState = true restoreState = true
} }
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
} }
}, },
label = { Text(stringResource(destination.label)) }, icon = {
alwaysShowLabel = false, Icon(
colors = NavigationBarItemDefaults.colors( imageVector = if (isCurrentDestOnBackStack) {
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant 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 { } else {
@@ -141,9 +215,7 @@ private fun BottomBar(navController: NavHostController) {
NavigationBarItem( NavigationBarItem(
selected = isCurrentDestOnBackStack, selected = isCurrentDestOnBackStack,
onClick = { onClick = {
if (isCurrentDestOnBackStack) { if (!isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) { navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) { popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true saveState = true
@@ -151,16 +223,25 @@ private fun BottomBar(navController: NavHostController) {
launchSingleTop = true launchSingleTop = true
restoreState = true restoreState = true
} }
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
} }
}, },
label = { Text(stringResource(destination.label)) }, icon = {
alwaysShowLabel = false, 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
)
}
) )
} }
} }

View File

@@ -48,7 +48,6 @@ fun ImageEditorDialog(
var offsetY by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val density = LocalDensity.current
var lastScale by remember { mutableFloatStateOf(1f) } var lastScale by remember { mutableFloatStateOf(1f) }
var lastOffsetX by remember { mutableFloatStateOf(0f) } var lastOffsetX by remember { mutableFloatStateOf(0f) }
var lastOffsetY by remember { mutableFloatStateOf(0f) } var lastOffsetY by remember { mutableFloatStateOf(0f) }

View File

@@ -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()
}
}

View File

@@ -63,7 +63,7 @@ fun SearchAppBar(
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
// 获取卡片颜色和透明度 // 获取卡片颜色和透明度
val cardColor = MaterialTheme.colorScheme.secondaryContainer val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
if (onSearch) { if (onSearch) {

View File

@@ -1,9 +1,12 @@
package com.sukisu.ultra.ui.component package com.sukisu.ultra.ui.component
import android.content.Context import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -11,15 +14,16 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme.getCardElevation import androidx.compose.material.icons.Icons
import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
/** /**
* 槽位选择对话框组件 * 槽位选择对话框组件
* 用于HorizonKernel刷写时选择目标槽位 * 用于Kernel刷写时选择目标槽位
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -43,38 +47,26 @@ fun SlotSelectionDialog(
} }
if (show) { if (show) {
val backgroundColor = if (!ThemeConfig.useDynamicColor) { val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast.copy(alpha = 1.0f) ThemeConfig.currentTheme.ButtonContrast
} else { } else {
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 1.0f) MaterialTheme.colorScheme.surfaceContainerHigh
} }
Dialog(onDismissRequest = onDismiss) { AlertDialog(
Card( onDismissRequest = onDismiss,
shape = MaterialTheme.shapes.medium.copy( title = {
topStart = CornerSize(16.dp),
topEnd = CornerSize(16.dp),
bottomEnd = CornerSize(16.dp),
bottomStart = CornerSize(16.dp)
),
colors = CardDefaults.cardColors(
containerColor = backgroundColor
),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Column(
modifier = Modifier
.padding(24.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text( Text(
text = stringResource(id = R.string.select_slot_title), text = stringResource(id = R.string.select_slot_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface
modifier = Modifier.padding(bottom = 16.dp)
) )
},
text = {
Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (errorMessage != null) { if (errorMessage != null) {
Text( Text(
text = "Error: $errorMessage", text = "Error: $errorMessage",
@@ -105,88 +97,134 @@ fun SlotSelectionDialog(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Horizontal arrangement for slot options with highlighted current slot
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalArrangement = Arrangement.spacedBy(6.dp) .fillMaxWidth()
) { .horizontalScroll(rememberScrollState()),
val isDefaultSlotA = currentSlot == "_a" || currentSlot == "a"
Button(
onClick = { onSlotSelected("a") },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = if (isDefaultSlotA)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.primaryContainer,
contentColor = if (isDefaultSlotA)
MaterialTheme.colorScheme.onPrimary
else
MaterialTheme.colorScheme.onPrimaryContainer
)
) {
Text(
text = stringResource(id = R.string.slot_a),
style = MaterialTheme.typography.labelLarge
)
}
val isDefaultSlotB = currentSlot == "_b" || currentSlot == "b"
Button(
onClick = { onSlotSelected("b") },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = if (isDefaultSlotB)
MaterialTheme.colorScheme.secondary
else
MaterialTheme.colorScheme.secondaryContainer,
contentColor = if (isDefaultSlotB)
MaterialTheme.colorScheme.onSecondary
else
MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Text(
text = stringResource(id = R.string.slot_b),
style = MaterialTheme.typography.labelLarge
)
}
}
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
thickness = 1.dp,
color = MaterialTheme.colorScheme.outline
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
TextButton( val slotOptions = listOf(
onClick = onDismiss, ListOption(
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) modifier = Modifier.weight(1f)
) { ) {
Text(text = stringResource(id = android.R.string.cancel)) 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( TextButton(
onClick = { onClick = {
currentSlot?.let { onSlotSelected(it) } currentSlot?.let { onSlotSelected(it) }
onDismiss() onDismiss()
}, }
modifier = Modifier.weight(1f)
) { ) {
Text(text = stringResource(id = android.R.string.ok)) Text(
} text = stringResource(android.R.string.ok),
} color = MaterialTheme.colorScheme.primary
} )
} }
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
} }
},
containerColor = cardColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
} }
} }
// 获取当前槽位信息 // Data class for list options
data class ListOption(
val titleText: String,
val subtitleText: String?,
val icon: ImageVector
)
// Utility function to get current slot
private fun getCurrentSlot(context: Context): String? { private fun getCurrentSlot(context: Context): String? {
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let { return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
if (it.startsWith("_")) it.substring(1) else it if (it.startsWith("_")) it.substring(1) else it

View File

@@ -1,32 +1,101 @@
package com.sukisu.ultra.ui.component package com.sukisu.ultra.ui.component
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable @Composable
fun SwitchItem( fun SwitchItem(
icon: ImageVector, icon: ImageVector,
title: String, title: String,
summary: String, summary: String? = null,
checked: Boolean, checked: Boolean,
modifier: Modifier = Modifier, enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit
) { ) {
// 颜色动画
val iconTint by animateColorAsState(
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
animationSpec = tween(300),
label = "iconTint"
)
// 开关颜色
val switchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
checkedBorderColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
ListItem( ListItem(
modifier = modifier, headlineContent = {
leadingContent = { Icon(icon, contentDescription = null) }, Text(
headlineContent = { Text(title) }, text = title,
supportingContent = { Text(summary) }, style = MaterialTheme.typography.titleMedium,
maxLines = 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 = { trailingContent = {
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = onCheckedChange onCheckedChange = null,
enabled = enabled,
colors = switchColors
) )
},
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled) {
onCheckedChange(!checked)
} }
.padding(vertical = 4.dp)
) )
} }

View File

@@ -1,7 +1,13 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
@@ -25,16 +31,21 @@ import androidx.compose.material.icons.filled.Android
import androidx.compose.material.icons.filled.Security import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -44,7 +55,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@@ -63,13 +77,13 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.SwitchItem import com.sukisu.ultra.ui.component.SwitchItem
import com.sukisu.ultra.ui.component.profile.AppProfileConfig import com.sukisu.ultra.ui.component.profile.AppProfileConfig
import com.sukisu.ultra.ui.component.profile.RootProfileConfig import com.sukisu.ultra.ui.component.profile.RootProfileConfig
import com.sukisu.ultra.ui.component.profile.TemplateConfig import com.sukisu.ultra.ui.component.profile.TemplateConfig
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.util.LocalSnackbarHost import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.forceStopApp import com.sukisu.ultra.ui.util.forceStopApp
import com.sukisu.ultra.ui.util.getSepolicy import com.sukisu.ultra.ui.util.getSepolicy
@@ -78,6 +92,7 @@ import com.sukisu.ultra.ui.util.restartApp
import com.sukisu.ultra.ui.util.setSepolicy import com.sukisu.ultra.ui.util.setSepolicy
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
import kotlinx.coroutines.launch
/** /**
* @author weishu * @author weishu
@@ -107,9 +122,18 @@ fun AppProfileScreen(
mutableStateOf(initialProfile) mutableStateOf(initialProfile)
} }
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
Scaffold( Scaffold(
topBar = { topBar = {
TopBar( TopBar(
title = appInfo.label,
packageName = packageName,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
onBack = dropUnlessResumed { navigator.popBackStack() }, onBack = dropUnlessResumed { navigator.popBackStack() },
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
@@ -181,22 +205,50 @@ private fun AppProfileInner(
val isRootGranted = profile.allowSu val isRootGranted = profile.allowSu
Column(modifier = modifier) { Column(modifier = modifier) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
AppMenuBox(packageName) { AppMenuBox(packageName) {
ListItem( ListItem(
headlineContent = { Text(appLabel) }, headlineContent = {
supportingContent = { Text(packageName) }, Text(
text = appLabel,
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Text(
text = packageName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingContent = appIcon, leadingContent = appIcon,
) )
} }
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
SwitchItem( SwitchItem(
icon = Icons.Filled.Security, icon = Icons.Filled.Security,
title = stringResource(id = R.string.superuser), title = stringResource(id = R.string.superuser),
checked = isRootGranted, checked = isRootGranted,
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
) )
}
Crossfade(targetState = isRootGranted, label = "") { current -> Crossfade(
targetState = isRootGranted,
label = "RootAccess"
) { current ->
Column( Column(
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
) { ) {
@@ -211,6 +263,13 @@ private fun AppProfileInner(
var mode by rememberSaveable { var mode by rememberSaveable {
mutableStateOf(initialMode) mutableStateOf(initialMode)
} }
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
ProfileBox(mode, true) { ProfileBox(mode, true) {
// template mode shouldn't change profile here! // template mode shouldn't change profile here!
if (it == Mode.Default || it == Mode.Custom) { if (it == Mode.Default || it == Mode.Custom) {
@@ -218,33 +277,73 @@ private fun AppProfileInner(
} }
mode = it mode = it
} }
Crossfade(targetState = mode, label = "") { currentMode -> }
if (currentMode == Mode.Template) {
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( TemplateConfig(
profile = profile, profile = profile,
onViewTemplate = onViewTemplate, onViewTemplate = onViewTemplate,
onManageTemplate = onManageTemplate, onManageTemplate = onManageTemplate,
onProfileChange = onProfileChange onProfileChange = onProfileChange
) )
} else if (mode == Mode.Custom) { }
Mode.Custom -> {
RootProfileConfig( RootProfileConfig(
fixedName = true, fixedName = true,
profile = profile, profile = profile,
onProfileChange = onProfileChange onProfileChange = onProfileChange
) )
} }
else -> {}
}
}
}
}
} }
} else { } else {
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
ProfileBox(mode, false) { ProfileBox(mode, false) {
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default))) onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
} }
Crossfade(targetState = mode, label = "") { currentMode -> }
val modifyEnabled = currentMode == Mode.Custom
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( AppProfileConfig(
fixedName = true, fixedName = true,
profile = profile, profile = profile,
enabled = modifyEnabled, enabled = mode == Mode.Custom,
onProfileChange = onProfileChange onProfileChange = onProfileChange
) )
} }
@@ -252,6 +351,8 @@ private fun AppProfileInner(
} }
} }
} }
}
}
} }
private enum class Mode(@StringRes private val res: Int) { private enum class Mode(@StringRes private val res: Int) {
@@ -264,20 +365,51 @@ private enum class Mode(@StringRes private val res: Int) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar( private fun TopBar(
title: String,
packageName: String,
onBack: () -> Unit, onBack: () -> Unit,
colors: TopAppBarColors,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
TopAppBar( TopAppBar(
title = { title = {
Text(stringResource(R.string.profile)) Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = packageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.alpha(0.8f)
)
}
}, },
colors = colors,
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = onBack onClick = onBack,
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
}, },
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), windowInsets = WindowInsets.safeDrawing.only(
scrollBehavior = scrollBehavior WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
scrollBehavior = scrollBehavior,
modifier = Modifier.shadow(
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
4.dp else 0.dp,
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
) )
} }
@@ -287,40 +419,88 @@ private fun ProfileBox(
hasTemplate: Boolean, hasTemplate: Boolean,
onModeChange: (Mode) -> Unit, onModeChange: (Mode) -> Unit,
) { ) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
ListItem( ListItem(
headlineContent = { Text(stringResource(R.string.profile)) }, headlineContent = {
supportingContent = { Text(mode.text) }, Text(
leadingContent = { Icon(Icons.Filled.AccountCircle, null) }, text = stringResource(R.string.profile),
style = MaterialTheme.typography.titleMedium
) )
HorizontalDivider(thickness = Dp.Hairline) },
ListItem(headlineContent = { 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( Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) { ) {
FilterChip( FilterChip(
selected = mode == Mode.Default, selected = mode == Mode.Default,
label = { Text(stringResource(R.string.profile_default)) },
onClick = { onModeChange(Mode.Default) }, onClick = { onModeChange(Mode.Default) },
label = {
Text(
text = stringResource(R.string.profile_default),
style = MaterialTheme.typography.bodyMedium
) )
},
shape = MaterialTheme.shapes.small
)
if (hasTemplate) { if (hasTemplate) {
FilterChip( FilterChip(
selected = mode == Mode.Template, selected = mode == Mode.Template,
label = { Text(stringResource(R.string.profile_template)) },
onClick = { onModeChange(Mode.Template) }, onClick = { onModeChange(Mode.Template) },
label = {
Text(
text = stringResource(R.string.profile_template),
style = MaterialTheme.typography.bodyMedium
)
},
shape = MaterialTheme.shapes.small
) )
} }
FilterChip( FilterChip(
selected = mode == Mode.Custom, selected = mode == Mode.Custom,
label = { Text(stringResource(R.string.profile_custom)) },
onClick = { onModeChange(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 @Composable
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
val density = LocalDensity.current val density = LocalDensity.current
@@ -329,13 +509,14 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures { detectTapGestures(
onLongPress = {
touchPoint = it touchPoint = it
expanded = true expanded = true
} }
)
} }
) { ) {
content() content()
val (offsetX, offsetY) = with(density) { val (offsetX, offsetY) = with(density) {
@@ -349,45 +530,64 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
expanded = false expanded = false
}, },
) { ) {
DropdownMenuItem( AppMenuOption(
text = { Text(stringResource(id = R.string.launch_app)) }, text = stringResource(id = R.string.launch_app),
onClick = { onClick = {
expanded = false expanded = false
launchApp(packageName) launchApp(packageName)
}, }
) )
DropdownMenuItem(
text = { Text(stringResource(id = R.string.force_stop_app)) }, AppMenuOption(
text = stringResource(id = R.string.force_stop_app),
onClick = { onClick = {
expanded = false expanded = false
forceStopApp(packageName) forceStopApp(packageName)
}, }
) )
DropdownMenuItem(
text = { Text(stringResource(id = R.string.restart_app)) }, AppMenuOption(
text = stringResource(id = R.string.restart_app),
onClick = { onClick = {
expanded = false expanded = false
restartApp(packageName) restartApp(packageName)
}, }
) )
} }
} }
}
@Composable
private fun AppMenuOption(text: String, onClick: () -> Unit) {
DropdownMenuItem(
text = {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium
)
},
onClick = onClick
)
} }
@Preview @Preview
@Composable @Composable
private fun AppProfilePreview() { private fun AppProfilePreview() {
var profile by remember { mutableStateOf(Natives.Profile("")) } var profile by remember { mutableStateOf(Natives.Profile("")) }
Surface {
AppProfileInner( AppProfileInner(
packageName = "icu.nullptr.test", packageName = "icu.nullptr.test",
appLabel = "Test", appLabel = "Test",
appIcon = { Icon(Icons.Filled.Android, null) }, appIcon = {
Icon(
imageVector = Icons.Filled.Android,
contentDescription = null,
)
},
profile = profile, profile = profile,
onProfileChange = { onProfileChange = {
profile = it profile = it
}, },
) )
}
} }

View File

@@ -5,59 +5,125 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.system.Os import android.system.Os
import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.* import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.Android
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.filled.Archive
import androidx.compose.material3.* import androidx.compose.material.icons.filled.Code
import androidx.compose.runtime.* import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers import com.sukisu.ultra.KernelVersion
import kotlinx.coroutines.withContext import com.sukisu.ultra.Natives
import com.sukisu.ultra.*
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.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 androidx.compose.ui.graphics.vector.ImageVector
import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.CardConfig
import androidx.core.content.edit import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.getKpmModuleCount
import com.sukisu.ultra.ui.util.getKpmVersion
import com.sukisu.ultra.ui.util.getModuleCount
import com.sukisu.ultra.ui.util.getSELinuxStatus
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.getSuSFSFeatures
import com.sukisu.ultra.ui.util.getSuSFSVariant
import com.sukisu.ultra.ui.util.getSuSFSVersion
import com.sukisu.ultra.ui.util.getSuperuserCount
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import com.sukisu.ultra.ui.util.reboot
import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import kotlin.random.Random import kotlin.random.Random
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
@Destination<RootGraph>(start = true) @Destination<RootGraph>(start = true)
@Composable @Composable
fun HomeScreen(navigator: DestinationsNavigator) { fun HomeScreen(navigator: DestinationsNavigator) {
@@ -66,32 +132,31 @@ fun HomeScreen(navigator: DestinationsNavigator) {
var isHideVersion by rememberSaveable { mutableStateOf(false) } var isHideVersion by rememberSaveable { mutableStateOf(false) }
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) } var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) } var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
var isHideLinkCard by rememberSaveable { mutableStateOf(false) }
// 从 SharedPreferences 加载简洁模式状态 // 从 SharedPreferences 加载简洁模式状态
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE) isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_simple_mode", false) .getBoolean("is_simple_mode", false)
}
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
LaunchedEffect(Unit) {
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE) isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_version", false) .getBoolean("is_hide_version", false)
}
// 从 SharedPreferences 加载隐藏模块数量等信息开关状态
LaunchedEffect(Unit) {
isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE) isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_other_info", false) .getBoolean("is_hide_other_info", false)
}
// 从 SharedPreferences 加载隐藏 SuSFS 状态开关状态
LaunchedEffect(Unit) {
isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE) isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false) .getBoolean("is_hide_susfs_status", false)
isHideLinkCard = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_link_card", false)
} }
val kernelVersion = getKernelVersion() val kernelVersion = getKernelVersion()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = Natives.becomeManager(ksuApp.packageName)
val deviceModel = getDeviceModel(context) val deviceModel = getDeviceModel()
val ksuVersion = if (isManager) Natives.version else null val ksuVersion = if (isManager) Natives.version else null
val zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel) val zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel)
val isVersion = ksuVersion == 12777 val isVersion = ksuVersion == 12777
@@ -108,12 +173,15 @@ fun HomeScreen(navigator: DestinationsNavigator) {
} }
} }
val scrollState = rememberScrollState()
val debounceTime = 100L
var lastScrollTime by remember { mutableLongStateOf(0L) }
Scaffold( Scaffold(
topBar = { topBar = {
TopBar( TopBar(
kernelVersion, kernelVersion,
onInstallClick = { navigator.navigate(InstallScreenDestination) }, onInstallClick = { navigator.navigate(InstallScreenDestination) },
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
}, },
@@ -124,11 +192,12 @@ fun HomeScreen(navigator: DestinationsNavigator) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(innerPadding) .padding(innerPadding)
.disableOverscroll()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()) .verticalScroll(scrollState)
.padding(top = 12.dp) .padding(top = 12.dp)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (shouldTriggerRestart) { if (shouldTriggerRestart) {
WarningCard(message = "zakozako") WarningCard(message = "zakozako")
@@ -143,6 +212,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
StatusCard(kernelVersion, ksuVersion, lkmMode) { StatusCard(kernelVersion, ksuVersion, lkmMode) {
navigator.navigate(InstallScreenDestination) navigator.navigate(InstallScreenDestination)
} }
if (isManager && Natives.requireNewKernel()) { if (isManager && Natives.requireNewKernel()) {
WarningCard( WarningCard(
stringResource(id = R.string.require_kernel_version).format( stringResource(id = R.string.require_kernel_version).format(
@@ -150,28 +220,39 @@ fun HomeScreen(navigator: DestinationsNavigator) {
) )
) )
} }
if (ksuVersion != null && !rootAvailable()) { if (ksuVersion != null && !rootAvailable()) {
WarningCard( WarningCard(
stringResource(id = R.string.grant_root_failed) stringResource(id = R.string.grant_root_failed)
) )
} }
val checkUpdate = val checkUpdate =
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true) .getBoolean("check_update", true)
if (checkUpdate) { if (checkUpdate) {
UpdateCard() UpdateCard()
} }
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) } val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) } var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
if (!isSimpleMode && clickCount < 3) { if (!isSimpleMode && clickCount < 3) {
AnimatedVisibility( AnimatedVisibility(
visible = clickCount < 3, visible = clickCount < 3,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -183,21 +264,44 @@ fun HomeScreen(navigator: DestinationsNavigator) {
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
modifier = Modifier.padding(end = 12.dp)
)
Text( Text(
text = stringResource(R.string.using_mksu_manager), text = stringResource(R.string.using_mksu_manager),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
) )
} }
} }
} }
} }
InfoCard() InfoCard()
if (!isSimpleMode) { if (!isSimpleMode) {
if (!isHideLinkCard) {
ContributionCard() ContributionCard()
DonateCard() DonateCard()
LearnMoreCard() LearnMoreCard()
} }
Spacer(Modifier) }
Spacer(Modifier.height(16.dp))
}
}
LaunchedEffect(scrollState) {
snapshotFlow { scrollState.isScrollInProgress }
.debounce(debounceTime)
.collect { isScrolling ->
if (isScrolling) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastScrollTime > debounceTime) {
lastScrollTime = currentTime
}
}
} }
} }
} }
@@ -217,25 +321,25 @@ fun UpdateCard() {
val newVersionUrl = newVersion.downloadUrl val newVersionUrl = newVersion.downloadUrl
val changelog = newVersion.changelog val changelog = newVersion.changelog
Log.d("UpdateCard", "Current version code: $currentVersionCode")
Log.d("UpdateCard", "New version code: $newVersionCode")
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val title = stringResource(id = R.string.module_changelog) val title = stringResource(id = R.string.module_changelog)
val updateText = stringResource(id = R.string.module_update) val updateText = stringResource(id = R.string.module_update)
AnimatedVisibility( AnimatedVisibility(
visible = newVersionCode > currentVersionCode, visible = newVersionCode > currentVersionCode,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard( WarningCard(
message = stringResource(id = R.string.new_version_available).format(newVersionCode), message = stringResource(id = R.string.new_version_available).format(newVersionCode),
MaterialTheme.colorScheme.outlineVariant color = MaterialTheme.colorScheme.tertiaryContainer,
) { onClick = {
if (changelog.isEmpty()) { if (changelog.isEmpty()) {
uriHandler.openUri(newVersionUrl) uriHandler.openUri(newVersionUrl)
} else { } else {
@@ -247,16 +351,22 @@ fun UpdateCard() {
) )
} }
} }
)
} }
} }
@Composable @Composable
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem(text = { DropdownMenuItem(
Text(stringResource(id)) text = { Text(stringResource(id)) },
}, onClick = { onClick = { reboot(reason) },
reboot(reason) leadingIcon = {
}) Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
)
}
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -264,38 +374,48 @@ fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
private fun TopBar( private fun TopBar(
kernelVersion: KernelVersion, kernelVersion: KernelVersion,
onInstallClick: () -> Unit, onInstallClick: () -> Unit,
onSettingsClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.secondaryContainer val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.app_name)) }, title = {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha), containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha) scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
), ),
actions = { actions = {
if (kernelVersion.isGKI()) { if (rootAvailable() || kernelVersion.isGKI()) {
IconButton(onClick = onInstallClick) { IconButton(onClick = onInstallClick) {
Icon(Icons.Filled.Archive, stringResource(R.string.install)) Icon(
Icons.Filled.Archive,
contentDescription = stringResource(R.string.install),
)
} }
} }
var showDropdown by remember { mutableStateOf(false) } var showDropdown by remember { mutableStateOf(false) }
if (Natives.isKsuValid(ksuApp.packageName)) { KsuIsValid {
IconButton(onClick = { showDropdown = true }) { IconButton(onClick = {
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot)) showDropdown = true
DropdownMenu( }) {
expanded = showDropdown, Icon(
onDismissRequest = { showDropdown = false } imageVector = Icons.Filled.Refresh,
) { contentDescription = stringResource(id = R.string.reboot)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
RebootDropdownItem(id = R.string.reboot) RebootDropdownItem(id = R.string.reboot)
val pm = val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { 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_userspace, reason = "userspace")
@@ -313,7 +433,6 @@ private fun TopBar(
) )
} }
@Composable @Composable
private fun StatusCard( private fun StatusCard(
kernelVersion: KernelVersion, kernelVersion: KernelVersion,
@@ -322,17 +441,28 @@ private fun StatusCard(
onClickInstall: () -> Unit = {} onClickInstall: () -> Unit = {}
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.1f)
)
) { ) {
Row(modifier = Modifier Row(
modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
if (kernelVersion.isGKI()) { if (rootAvailable() || kernelVersion.isGKI()) {
onClickInstall() onClickInstall()
} }
} }
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) { .padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
when { when {
ksuVersion != null -> { ksuVersion != null -> {
val safeMode = when { val safeMode = when {
@@ -346,8 +476,7 @@ private fun StatusCard(
else -> " <GKI>" else -> " <GKI>"
} }
val workingText = val workingText = "${stringResource(id = R.string.home_working)}$workingMode$safeMode"
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_version", false) .getBoolean("is_hide_version", false)
@@ -358,40 +487,55 @@ private fun StatusCard(
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false) .getBoolean("is_hide_susfs_status", false)
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working)) Icon(
Icons.Outlined.CheckCircle,
contentDescription = stringResource(R.string.home_working),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
Text( Text(
text = workingText, text = workingText,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
) )
if (!isHideVersion) { if (!isHideVersion) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_working_version, ksuVersion), text = stringResource(R.string.home_working_version, ksuVersion),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
if (!isHideOtherInfo) { if (!isHideOtherInfo) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource( text = stringResource(R.string.home_superuser_count, getSuperuserCount()),
R.string.home_superuser_count, getSuperuserCount() style = MaterialTheme.typography.bodyMedium,
), style = MaterialTheme.typography.bodyMedium color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_module_count, getModuleCount()), text = stringResource(R.string.home_module_count, getModuleCount()),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) { if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()), text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
if (!isHideSusfsStatus) { if (!isHideSusfsStatus) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -405,7 +549,8 @@ private fun StatusCard(
Text( Text(
text = stringResource(R.string.home_susfs, translatedStatus), text = stringResource(R.string.home_susfs, translatedStatus),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -413,31 +558,49 @@ private fun StatusCard(
} }
kernelVersion.isGKI() -> { kernelVersion.isGKI() -> {
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed)) Icon(
Icons.Outlined.Warning,
contentDescription = stringResource(R.string.home_not_installed),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp)
)
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
Text( Text(
text = stringResource(R.string.home_not_installed), text = stringResource(R.string.home_not_installed),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_click_to_install), text = stringResource(R.string.home_click_to_install),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
else -> { else -> {
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported)) Icon(
Icons.Outlined.Block,
contentDescription = stringResource(R.string.home_unsupported),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp)
)
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
Text( Text(
text = stringResource(R.string.home_unsupported), text = stringResource(R.string.home_unsupported),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_unsupported_reason), text = stringResource(R.string.home_unsupported_reason),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -448,31 +611,63 @@ private fun StatusCard(
@Composable @Composable
fun WarningCard( fun WarningCard(
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null message: String,
color: Color = MaterialTheme.colorScheme.errorContainer,
onClick: (() -> Unit)? = null
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(color),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.error.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier) .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.padding(24.dp) .padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier
.padding(end = 16.dp)
.size(28.dp)
)
Text( Text(
text = message, style = MaterialTheme.typography.bodyMedium text = message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
) )
} }
} }
} }
@Composable @Composable
fun ContributionCard() { fun ContributionCard() {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val links = listOf("https://github.com/zako", "https://github.com/udochina") val links = listOf("https://github.com/ShirkNeko", "https://github.com/udochina")
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -487,12 +682,15 @@ fun ContributionCard() {
Column { Column {
Text( Text(
text = stringResource(R.string.home_ContributionCard_kernelsu), text = stringResource(R.string.home_ContributionCard_kernelsu),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu), text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -505,25 +703,38 @@ fun LearnMoreCard() {
val url = stringResource(R.string.home_learn_kernelsu_url) val url = stringResource(R.string.home_learn_kernelsu_url)
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Row(
Row(modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
uriHandler.openUri(url) uriHandler.openUri(url)
} }
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) { .padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column { Column {
Text( Text(
text = stringResource(R.string.home_learn_kernelsu), text = stringResource(R.string.home_learn_kernelsu),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_click_to_learn_kernelsu), text = stringResource(R.string.home_click_to_learn_kernelsu),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -535,25 +746,38 @@ fun DonateCard() {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f)
)
) { ) {
Row(
Row(modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
uriHandler.openUri("https://patreon.com/weishu") uriHandler.openUri("https://patreon.com/weishu")
} }
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) { .padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column { Column {
Text( Text(
text = stringResource(R.string.home_support_title), text = stringResource(R.string.home_support_title),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_support_content), text = stringResource(R.string.home_support_content),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -568,8 +792,16 @@ private fun InfoCard() {
.getBoolean("is_simple_mode", false) .getBoolean("is_simple_mode", false)
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHighest),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.05f)
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -586,57 +818,95 @@ private fun InfoCard() {
icon: ImageVector = Icons.Default.Info icon: ImageVector = Icons.Default.Info
) { ) {
contents.appendLine(label).appendLine(content).appendLine() contents.appendLine(label).appendLine(content).appendLine()
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = label, contentDescription = label,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column { Column(
Text(text = label, style = MaterialTheme.typography.bodyLarge) modifier = Modifier
Text(text = content, style = MaterialTheme.typography.bodyMedium) .fillMaxWidth()
.weight(1f)
){
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = content,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
softWrap = true
)
} }
} }
} }
InfoCardItem(stringResource(R.string.home_kernel), uname.release, icon = Icons.Default.Memory) InfoCardItem(
stringResource(R.string.home_kernel),
uname.release,
icon = Icons.Default.Memory,
)
if (!isSimpleMode) { if (!isSimpleMode) {
Spacer(Modifier.height(16.dp))
val androidVersion = Build.VERSION.RELEASE val androidVersion = Build.VERSION.RELEASE
InfoCardItem(stringResource(R.string.home_android_version), androidVersion, icon = Icons.Default.Android) InfoCardItem(
stringResource(R.string.home_android_version),
androidVersion,
icon = Icons.Default.Android,
)
} }
Spacer(Modifier.height(16.dp)) val deviceModel = getDeviceModel()
val deviceModel = getDeviceModel(context) InfoCardItem(
InfoCardItem(stringResource(R.string.home_device_model), deviceModel, icon = Icons.Default.PhoneAndroid) stringResource(R.string.home_device_model),
deviceModel,
icon = Icons.Default.PhoneAndroid,
)
Spacer(Modifier.height(16.dp))
val managerVersion = getManagerVersion(context) val managerVersion = getManagerVersion(context)
InfoCardItem(stringResource(R.string.home_manager_version), "${managerVersion.first} (${managerVersion.second})", icon = Icons.Default.Settings) InfoCardItem(
stringResource(R.string.home_manager_version),
"${managerVersion.first} (${managerVersion.second})",
icon = Icons.Default.Settings,
)
Spacer(Modifier.height(16.dp)) InfoCardItem(
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus(), icon = Icons.Default.Security) stringResource(R.string.home_selinux_status),
getSELinuxStatus(),
icon = Icons.Default.Security,
)
if (!isSimpleMode) { if (!isSimpleMode) {
if (lkmMode != true) { if (lkmMode != true) {
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
var displayVersion: String
val isKpmConfigured = checkKpmConfigured() val isKpmConfigured = checkKpmConfigured()
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) { val displayVersion = if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
val statusText = if (isKpmConfigured) { val statusText = if (isKpmConfigured) {
stringResource(R.string.kernel_patched) stringResource(R.string.kernel_patched)
} else { } else {
stringResource(R.string.kernel_not_enabled) stringResource(R.string.kernel_not_enabled)
} }
displayVersion = "${stringResource(R.string.not_supported)} ($statusText)" "${stringResource(R.string.not_supported)} ($statusText)"
} else { } else {
displayVersion = "${stringResource(R.string.supported)} ($kpmVersion)" "${stringResource(R.string.supported)} ($kpmVersion)"
} }
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_kpm_version), displayVersion, icon = Icons.Default.Code) InfoCardItem(
stringResource(R.string.home_kpm_version),
displayVersion,
icon = Icons.Default.Code
)
} }
} }
@@ -644,12 +914,10 @@ private fun InfoCard() {
.getBoolean("is_hide_susfs_status", false) .getBoolean("is_hide_susfs_status", false)
if ((!isSimpleMode) && (!isHideSusfsStatus)) { if ((!isSimpleMode) && (!isHideSusfsStatus)) {
Spacer(modifier = Modifier.height(16.dp))
val suSFS = getSuSFS() val suSFS = getSuSFS()
if (suSFS == "Supported") { if (suSFS == "Supported") {
val suSFSVersion = getSuSFSVersion() val suSFSVersion = getSuSFSVersion()
if (suSFSVersion.isEmpty()) return@withContext if (suSFSVersion.isNotEmpty()) {
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU" val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
val infoText = buildString { val infoText = buildString {
append(suSFSVersion) append(suSFSVersion)
@@ -661,8 +929,13 @@ private fun InfoCard() {
} }
} }
} }
InfoCardItem( InfoCardItem(
stringResource(R.string.home_susfs_version), infoText, icon = Icons.Default.Storage) stringResource(R.string.home_susfs_version),
infoText,
icon = Icons.Default.Storage
)
}
} }
} }
} }
@@ -678,7 +951,7 @@ fun getManagerVersion(context: Context): Pair<String, Long> {
@Preview @Preview
@Composable @Composable
private fun StatusCardPreview() { private fun StatusCardPreview() {
Column { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
StatusCard(KernelVersion(5, 10, 101), 1, null) StatusCard(KernelVersion(5, 10, 101), 1, null)
StatusCard(KernelVersion(5, 10, 101), 20000, true) StatusCard(KernelVersion(5, 10, 101), 20000, true)
StatusCard(KernelVersion(5, 10, 101), null, true) StatusCard(KernelVersion(5, 10, 101), null, true)
@@ -689,17 +962,17 @@ private fun StatusCardPreview() {
@Preview @Preview
@Composable @Composable
private fun WarningCardPreview() { private fun WarningCardPreview() {
Column { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
WarningCard(message = "Warning message") WarningCard(message = "Warning message")
WarningCard( WarningCard(
message = "Warning message ", message = "Warning message ",
MaterialTheme.colorScheme.outlineVariant, MaterialTheme.colorScheme.tertiaryContainer,
onClick = {}) onClick = {})
} }
} }
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
private fun getDeviceModel(context: Context): String { private fun getDeviceModel(): String {
return try { return try {
val systemProperties = Class.forName("android.os.SystemProperties") val systemProperties = Class.forName("android.os.SystemProperties")
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java) val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
@@ -740,3 +1013,12 @@ private fun checkKpmConfigured(): Boolean {
} }
return false return false
} }
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.disableOverscroll(): Modifier = composed {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
this
} else {
this
}
}

View File

@@ -15,42 +15,86 @@ import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material3.* import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.* import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
import com.sukisu.ultra.flash.HorizonKernelState
import com.sukisu.ultra.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.component.DialogHandle import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog import com.sukisu.ultra.ui.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.flash.HorizonKernelFlashProgress import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.flash.HorizonKernelState import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.LkmSelection
import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.getCurrentKmi
import com.sukisu.ultra.ui.util.getSupportedKmis
import com.sukisu.ultra.ui.util.isAbDevice
import com.sukisu.ultra.ui.util.isInitBoot
import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.getKernelVersion
/** /**
* @author weishu * @author weishu
@@ -69,6 +113,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
val horizonKernelState = remember { HorizonKernelState() } val horizonKernelState = remember { HorizonKernelState() }
val flashState by horizonKernelState.state.collectAsState() val flashState by horizonKernelState.state.collectAsState()
val summary = stringResource(R.string.horizon_kernel_summary) val summary = stringResource(R.string.horizon_kernel_summary)
val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice()
val onFlashComplete = { val onFlashComplete = {
showRebootDialog = true showRebootDialog = true
@@ -123,7 +170,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
// 槽位选择 // 槽位选择
SlotSelectionDialog( SlotSelectionDialog(
show = showSlotSelectionDialog, show = showSlotSelectionDialog && isAbDevice,
onDismiss = { showSlotSelectionDialog = false }, onDismiss = { showSlotSelectionDialog = false },
onSlotSelected = { slot -> onSlotSelected = { slot ->
showSlotSelectionDialog = false showSlotSelectionDialog = false
@@ -133,7 +180,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
summary = summary summary = summary
) )
installMethod = horizonMethod installMethod = horizonMethod
onInstall()
} }
) )
@@ -149,7 +195,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} }
val onClickNext = { val onClickNext = {
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
selectKmiDialog.show() selectKmiDialog.show()
} else { } else {
onInstall() onInstall()
@@ -191,15 +237,22 @@ fun InstallScreen(navigator: DestinationsNavigator) {
.padding(innerPadding) .padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(top = 12.dp)
) { ) {
SelectInstallMethod( SelectInstallMethod(
isGKI = isGKI,
isAbDevice = isAbDevice,
onSelected = { method -> onSelected = { method ->
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) { if (method is InstallMethod.HorizonKernel && method.uri != null) {
if (isAbDevice) {
tempKernelUri = method.uri tempKernelUri = method.uri
showSlotSelectionDialog = true showSlotSelectionDialog = true
} else { } else {
installMethod = method installMethod = method
} }
} else {
installMethod = method
}
horizonKernelState.reset() horizonKernelState.reset()
} }
) )
@@ -218,32 +271,73 @@ fun InstallScreen(navigator: DestinationsNavigator) {
.padding(16.dp) .padding(16.dp)
) { ) {
(lkmSelection as? LkmSelection.LkmUri)?.let { (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( text = stringResource(
id = R.string.selected_lkm, id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)" it.uri.lastPathSegment ?: "(file)"
) ),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
) )
} }
}
(installMethod as? InstallMethod.HorizonKernel)?.let { method -> (installMethod as? InstallMethod.HorizonKernel)?.let { method ->
if (method.slot != null) { 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( text = stringResource(
id = R.string.selected_slot, id = R.string.selected_slot,
if (method.slot == "a") stringResource(id = R.string.slot_a) if (method.slot == "a") stringResource(id = R.string.slot_a)
else stringResource(id = R.string.slot_b) else stringResource(id = R.string.slot_b)
) ),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
) )
} }
} }
}
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null && !flashState.isFlashing, enabled = installMethod != null && !flashState.isFlashing,
onClick = onClickNext onClick = onClickNext,
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
) { ) {
Text( Text(
stringResource(id = R.string.install_next), stringResource(id = R.string.install_next),
fontSize = MaterialTheme.typography.bodyMedium.fontSize style = MaterialTheme.typography.bodyMedium
) )
} }
} }
@@ -305,7 +399,11 @@ sealed class InstallMethod {
} }
@Composable @Composable
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { private fun SelectInstallMethod(
isGKI: Boolean = false,
isAbDevice: Boolean = false,
onSelected: (InstallMethod) -> Unit = {}
) {
val rootAvailable = rootAvailable() val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice() val isAbDevice = isAbDevice()
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
@@ -335,8 +433,16 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
if (it.resultCode == Activity.RESULT_OK) { if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> it.data?.data?.let { uri ->
val option = when (currentSelectingMethod) { val option = when (currentSelectingMethod) {
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip) is InstallMethod.SelectFile -> InstallMethod.SelectFile(
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = horizonKernelSummary) uri,
summary = selectFileTip
)
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
uri,
summary = horizonKernelSummary
)
else -> null else -> null
} }
option?.let { option?.let {
@@ -364,22 +470,89 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> { is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/*" type = "application/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip")) putExtra(
Intent.EXTRA_MIME_TYPES,
arrayOf("application/octet-stream", "application/zip")
)
}) })
} }
is InstallMethod.DirectInstall -> { is InstallMethod.DirectInstall -> {
selectedOption = option selectedOption = option
onSelected(option) onSelected(option)
} }
is InstallMethod.DirectInstallToInactiveSlot -> { is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent) confirmDialog.showConfirm(dialogTitle, dialogContent)
} }
} }
} }
Column { var LKMExpanded by remember { mutableStateOf(false) }
radioOptions.forEach { option -> var GKIExpanded by remember { mutableStateOf(false) }
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() } 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -391,33 +564,148 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
indication = LocalIndication.current, indication = LocalIndication.current,
interactionSource = interactionSource interactionSource = interactionSource
) )
.padding(vertical = 8.dp, horizontal = 12.dp)
) { ) {
RadioButton( RadioButton(
selected = option.javaClass == selectedOption?.javaClass, selected = option.javaClass == selectedOption?.javaClass,
onClick = { onClick(option) }, onClick = null,
interactionSource = interactionSource interactionSource = interactionSource,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colorScheme.primary,
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
)
) )
Column( Column(
modifier = Modifier.padding(vertical = 12.dp) modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) { ) {
Text( Text(
text = stringResource(id = option.label), text = stringResource(id = option.label),
fontSize = MaterialTheme.typography.titleMedium.fontSize, style = MaterialTheme.typography.bodyLarge
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
) )
option.summary?.let { option.summary?.let {
Text( Text(
text = it, text = it,
fontSize = MaterialTheme.typography.bodySmall.fontSize, style = MaterialTheme.typography.bodySmall,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily, color = MaterialTheme.colorScheme.onSurfaceVariant
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
) )
} }
} }
} }
} }
} }
}
}
}
}
// 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) @OptIn(ExperimentalMaterial3Api::class)
@@ -427,77 +715,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
val supportedKmi by produceState(initialValue = emptyList<String>()) { val supportedKmi by produceState(initialValue = emptyList<String>()) {
value = getSupportedKmis() value = getSupportedKmis()
} }
val listOptions = supportedKmi.map { value -> val options = supportedKmi.map { value ->
ListOption( ListOption(
titleText = value, titleText = value
subtitleText = null,
icon = null
) )
} }
var selection: String? = null var selection by remember { mutableStateOf<String?>(null) }
val cardColor = if (!ThemeConfig.useDynamicColor) { val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
AlertDialog( MaterialTheme(
onDismissRequest = { colorScheme = MaterialTheme.colorScheme.copy(
dismiss() surface = backgroundColor
}, )
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 { ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
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) onSelected(selection)
} }, onCloseRequest = {
dismiss() dismiss()
}), header = Header.Default(
title = stringResource(R.string.select_kmi),
), selection = ListSelection.Single(
showRadioButtons = true,
options = options,
) { _, option ->
selection = option.titleText
})
} }
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
}
) {
Text(text = stringResource(android.R.string.cancel))
}
},
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.medium,
tonalElevation = getCardElevation()
)
} }
} }
@@ -508,23 +752,26 @@ private fun TopBar(
onLkmUpload: () -> Unit = {}, onLkmUpload: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.secondaryContainer val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha val cardAlpha = cardAlpha
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.install)) }, title = {
Text(
stringResource(R.string.install),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha), containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha) scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
), ),
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) Icon(
} Icons.AutoMirrored.Filled.ArrowBack,
}, contentDescription = stringResource(R.string.back)
actions = { )
IconButton(onClick = onLkmUpload) {
Icon(Icons.Filled.FileUpload, contentDescription = null)
} }
}, },
windowInsets = WindowInsets.safeDrawing.only( windowInsets = WindowInsets.safeDrawing.only(

View File

@@ -1,6 +1,5 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
@@ -10,11 +9,13 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -36,6 +37,8 @@ import java.io.BufferedReader
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.* import java.net.*
import android.app.Activity
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
/** /**
* KPM 管理界面 * KPM 管理界面
@@ -53,11 +56,6 @@ fun KpmScreen(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() } val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog() val confirmDialog = rememberConfirmDialog()
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
val moduleConfirmContentMap = viewModel.moduleList.associate { module -> val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
val moduleFileName = module.id val moduleFileName = module.id
@@ -103,16 +101,33 @@ fun KpmScreen(
} }
} }
} }
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {
dismiss() dismiss()
tempFileForInstall?.delete() tempFileForInstall?.delete()
tempFileForInstall = null tempFileForInstall = null
}, },
title = { Text(kpmInstallMode) }, title = {
text = { moduleName?.let { Text(stringResource(R.string.kpm_install_mode_description, it)) } }, Text(
confirmButton = { text = kpmInstallMode,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Column { Column {
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( Button(
onClick = { onClick = {
scope.launch { scope.launch {
@@ -129,11 +144,20 @@ fun KpmScreen(
} }
tempFileForInstall = null 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) Text(kpmInstallModeLoad)
} }
Spacer(modifier = Modifier.height(8.dp))
Button( Button(
onClick = { onClick = {
scope.launch { scope.launch {
@@ -150,13 +174,30 @@ fun KpmScreen(
} }
tempFileForInstall = null 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) Text(kpmInstallModeEmbed)
} }
} }
}
},
confirmButton = {
}, },
dismissButton = { dismissButton = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
TextButton( TextButton(
onClick = { onClick = {
dismiss() dismiss()
@@ -167,13 +208,16 @@ fun KpmScreen(
Text(cancel) Text(cancel)
} }
} }
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge
) )
} }
val selectPatchLauncher = rememberLauncherForActivityResult( val selectPatchLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
val uri = result.data?.data ?: return@rememberLauncherForActivityResult val uri = result.data?.data ?: return@rememberLauncherForActivityResult
@@ -236,10 +280,12 @@ fun KpmScreen(
onClearClick = { viewModel.search = "" }, onClearClick = { viewModel.search = "" },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
dropdownContent = { dropdownContent = {
IconButton(onClick = { viewModel.fetchModuleList() }) { IconButton(
onClick = { viewModel.fetchModuleList() }
) {
Icon( Icon(
imageVector = Icons.Outlined.Refresh, imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.refresh) contentDescription = stringResource(R.string.refresh),
) )
} }
} }
@@ -256,19 +302,33 @@ fun KpmScreen(
}, },
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.Add, imageVector = Icons.Filled.Add,
contentDescription = stringResource(R.string.kpm_install) contentDescription = stringResource(R.string.kpm_install),
) )
}, },
text = { Text(stringResource(R.string.kpm_install)) }, text = {
containerColor = cardColor.copy(alpha = 1f), Text(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer text = stringResource(R.string.kpm_install),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
},
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
) )
}, },
snackbarHost = { SnackbarHost(snackBarHost) } snackbarHost = { SnackbarHost(snackBarHost) }
) { padding -> ) { padding ->
Column(modifier = Modifier.padding(padding)) { Column(modifier = Modifier.padding(padding)) {
if (!isNoticeClosed) { if (!isNoticeClosed) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(MaterialTheme.shapes.medium)
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -276,37 +336,69 @@ fun KpmScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Text( Text(
text = stringResource(R.string.kernel_module_notice), text = stringResource(R.string.kernel_module_notice),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = TextAlign.Center style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
) )
IconButton(onClick = {
IconButton(
onClick = {
isNoticeClosed = true isNoticeClosed = true
sharedPreferences.edit { putBoolean("is_notice_closed", true) } sharedPreferences.edit { putBoolean("is_notice_closed", true) }
}) { },
modifier = Modifier.size(24.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Icon( Icon(
imageVector = Icons.Outlined.Close, imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_notice) contentDescription = stringResource(R.string.close_notice)
) )
} }
} }
} }
}
if (viewModel.moduleList.isEmpty()) { if (viewModel.moduleList.isEmpty()) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center 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( Text(
stringResource(R.string.kpm_empty), stringResource(R.string.kpm_empty),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
items(viewModel.moduleList) { module -> items(viewModel.moduleList) { module ->
@@ -489,13 +581,34 @@ private fun KpmModuleItem(
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) { if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
AlertDialog( AlertDialog(
onDismissRequest = { viewModel.hideInputDialog() }, onDismissRequest = { viewModel.hideInputDialog() },
title = { Text(stringResource(R.string.kpm_control)) }, title = {
Text(
text = stringResource(R.string.kpm_control),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = { text = {
OutlinedTextField( OutlinedTextField(
value = viewModel.inputArgs, value = viewModel.inputArgs,
onValueChange = { viewModel.updateInputArgs(it) }, onValueChange = { viewModel.updateInputArgs(it) },
label = { Text(stringResource(R.string.kpm_args)) }, label = {
placeholder = { Text(module.args) } Text(
text = stringResource(R.string.kpm_args),
color = MaterialTheme.colorScheme.primary
)
},
placeholder = {
Text(
text = module.args,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
},
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
) )
}, },
confirmButton = { confirmButton = {
@@ -512,23 +625,39 @@ private fun KpmModuleItem(
} }
} }
) { ) {
Text(stringResource(R.string.confirm)) Text(
text = stringResource(R.string.confirm),
color = MaterialTheme.colorScheme.primary
)
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { viewModel.hideInputDialog() }) { TextButton(onClick = { viewModel.hideInputDialog() }) {
Text(stringResource(R.string.cancel)) Text(
} text = stringResource(R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
} }
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge
) )
} }
ElevatedCard( Card(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(20.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -538,54 +667,77 @@ private fun KpmModuleItem(
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = module.name, text = module.name,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "${stringResource(R.string.kpm_version)}: ${module.version}", text = "${stringResource(R.string.kpm_version)}: ${module.version}",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = "${stringResource(R.string.kpm_author)}: ${module.author}", text = "${stringResource(R.string.kpm_author)}: ${module.author}",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = "${stringResource(R.string.kpm_args)}: ${module.args}", text = "${stringResource(R.string.kpm_args)}: ${module.args}",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(12.dp))
Text( Text(
text = module.description, text = module.description,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(20.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
FilledTonalButton( Button(
onClick = { viewModel.showInputDialog(module.id) }, onClick = { viewModel.showInputDialog(module.id) },
enabled = module.hasAction enabled = module.hasAction,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Settings, imageVector = Icons.Filled.Settings,
contentDescription = null contentDescription = null,
modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.kpm_control)) Text(stringResource(R.string.kpm_control))
} }
FilledTonalButton( Button(
onClick = onUninstall onClick = onUninstall,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Delete, imageVector = Icons.Filled.Delete,
contentDescription = null contentDescription = null,
modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.kpm_uninstall)) Text(stringResource(R.string.kpm_uninstall))
} }
} }

View File

@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.* import androidx.compose.material.icons.automirrored.outlined.*
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -105,15 +63,14 @@ import com.sukisu.ultra.ui.webui.WebUIActivity
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import com.sukisu.ultra.ui.util.ModuleModify import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -239,7 +196,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
@@ -275,7 +232,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) { ) {
Icon( Icon(
imageVector = Icons.Filled.MoreVert, imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings) contentDescription = stringResource(id = R.string.settings),
) )
DropdownMenu( DropdownMenu(
@@ -284,7 +241,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_action_first)) }, text = { Text(stringResource(R.string.module_sort_action_first)) },
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) }, trailingIcon = {
Checkbox(
checked = viewModel.sortActionFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = { onClick = {
viewModel.sortActionFirst = !viewModel.sortActionFirst viewModel.sortActionFirst = !viewModel.sortActionFirst
prefs.edit { prefs.edit {
@@ -300,7 +266,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_enabled_first)) }, text = { Text(stringResource(R.string.module_sort_enabled_first)) },
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) }, trailingIcon = {
Checkbox(
checked = viewModel.sortEnabledFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = { onClick = {
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
prefs.edit { prefs.edit {
@@ -311,12 +286,13 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} }
} }
) )
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.backup_modules)) }, text = { Text(stringResource(R.string.backup_modules)) },
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Download, imageVector = Icons.Outlined.Download,
contentDescription = stringResource(R.string.backup) contentDescription = stringResource(R.string.backup),
) )
}, },
onClick = { onClick = {
@@ -329,7 +305,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Refresh, imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.restore) contentDescription = stringResource(R.string.restore),
) )
}, },
onClick = { onClick = {
@@ -346,11 +322,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
floatingActionButton = { floatingActionButton = {
if (!hideInstallButton) { if (!hideInstallButton) {
val moduleInstall = stringResource(id = R.string.module_install) val moduleInstall = stringResource(id = R.string.module_install)
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = {
selectZipLauncher.launch( selectZipLauncher.launch(
@@ -363,16 +334,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
icon = { icon = {
Icon( Icon(
imageVector = Icons.Filled.Add, imageVector = Icons.Filled.Add,
contentDescription = moduleInstall contentDescription = moduleInstall,
) )
}, },
text = { text = {
Text( Text(
text = moduleInstall text = moduleInstall,
color = MaterialTheme.colorScheme.onPrimaryContainer
) )
}, },
containerColor = cardColor.copy(alpha = 1f), contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer expanded = true,
) )
} }
}, },
@@ -389,12 +361,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
.padding(24.dp), .padding(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier
.size(64.dp)
.padding(bottom = 16.dp)
)
Text( Text(
stringResource(R.string.module_magisk_conflict), stringResource(R.string.module_magisk_conflict),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
}
else -> { else -> {
ModuleList( ModuleList(
navigator = navigator, navigator = navigator,
@@ -591,10 +578,25 @@ private fun ModuleList(
modifier = Modifier.fillParentMaxSize(), modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Column(
stringResource(R.string.module_empty), horizontalAlignment = Alignment.CenterHorizontally,
textAlign = TextAlign.Center verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Outlined.Extension,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
) )
Text(
text = stringResource(R.string.module_empty),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
@@ -677,8 +679,16 @@ fun ModuleItem(
onClick: (ModuleViewModel.ModuleInfo) -> Unit onClick: (ModuleViewModel.ModuleInfo) -> Unit
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation()) elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@@ -706,6 +716,7 @@ fun ModuleItem(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
val moduleVersion = stringResource(id = R.string.module_version) val moduleVersion = stringResource(id = R.string.module_version)
val moduleAuthor = stringResource(id = R.string.module_author) val moduleAuthor = stringResource(id = R.string.module_author)
@@ -720,6 +731,7 @@ fun ModuleItem(
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily, fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
textDecoration = textDecoration, textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurface
) )
Text( Text(
@@ -727,7 +739,8 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.bodySmall.fontSize, fontSize = MaterialTheme.typography.bodySmall.fontSize,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily, fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
@@ -735,7 +748,8 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.bodySmall.fontSize, fontSize = MaterialTheme.typography.bodySmall.fontSize,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily, fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@@ -749,7 +763,15 @@ fun ModuleItem(
enabled = !module.update, enabled = !module.update,
checked = module.enabled, checked = module.enabled,
onCheckedChange = onCheckChanged, onCheckedChange = onCheckChanged,
interactionSource = if (!module.hasWebUi) interactionSource else null interactionSource = if (!module.hasWebUi) interactionSource else null,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
) )
} }
} }
@@ -764,83 +786,70 @@ fun ModuleItem(
fontWeight = MaterialTheme.typography.bodySmall.fontWeight, fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 4, maxLines = 4,
textDecoration = textDecoration textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(thickness = Dp.Hairline) HorizontalDivider(thickness = Dp.Hairline)
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (module.hasActionScript) { if (module.hasActionScript) {
FilledTonalButton( FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp), modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove && module.enabled, enabled = !module.remove && module.enabled,
onClick = { onClick = {
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId)) navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
viewModel.markNeedRefresh() viewModel.markNeedRefresh()
}, },
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) { colors = ButtonDefaults.filledTonalButtonColors()
ButtonDefaults.filledTonalButtonColors(
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.PlayArrow, imageVector = Icons.Outlined.PlayArrow,
contentDescription = null contentDescription = null
) )
if (!module.hasWebUi && updateUrl.isEmpty()) { //if (!module.hasWebUi && updateUrl.isEmpty()) {
Text( //Text(
modifier = Modifier.padding(start = 7.dp), // modifier = Modifier.padding(start = 7.dp),
text = stringResource(R.string.action), // text = stringResource(R.string.action),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily, // fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize // fontSize = MaterialTheme.typography.labelMedium.fontSize
) //)
//}
} }
} }
Spacer(modifier = Modifier.weight(0.1f, true))
}
if (module.hasWebUi) { if (module.hasWebUi) {
FilledTonalButton( FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp), modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove && module.enabled, enabled = !module.remove && module.enabled,
onClick = { onClick(module) }, onClick = { onClick(module) },
interactionSource = interactionSource, interactionSource = interactionSource,
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) { colors = ButtonDefaults.filledTonalButtonColors()
ButtonDefaults.filledTonalButtonColors(
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
contentDescription = null contentDescription = null
) )
if (!module.hasActionScript && updateUrl.isEmpty()) { //if (!module.hasActionScript && updateUrl.isEmpty()) {
Text( //Text(
modifier = Modifier.padding(start = 7.dp), // modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily, // fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize, // fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.open) // text = stringResource(R.string.open)
) //)
} //}
} }
} }
@@ -848,7 +857,7 @@ fun ModuleItem(
if (updateUrl.isNotEmpty()) { if (updateUrl.isNotEmpty()) {
Button( Button(
modifier = Modifier.defaultMinSize(52.dp, 32.dp), modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove, enabled = !module.remove,
onClick = { onUpdate(module) }, onClick = { onUpdate(module) },
shape = ButtonDefaults.textShape, shape = ButtonDefaults.textShape,
@@ -859,30 +868,23 @@ fun ModuleItem(
imageVector = Icons.Outlined.Download, imageVector = Icons.Outlined.Download,
contentDescription = null contentDescription = null
) )
if (!module.hasActionScript || !module.hasWebUi) { //if (!module.hasActionScript || !module.hasWebUi) {
Text( //Text(
modifier = Modifier.padding(start = 7.dp), // modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily, // fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize, // fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.module_update) // text = stringResource(R.string.module_update)
) //)
//}
} }
} }
Spacer(modifier = Modifier.weight(0.1f, true))
}
FilledTonalButton( FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp), modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
onClick = { onUninstallClicked(module) }, onClick = { onUninstallClicked(module) },
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) { colors = ButtonDefaults.filledTonalButtonColors(
ButtonDefaults.filledTonalButtonColors( containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
) { ) {
if (!module.remove) { if (!module.remove) {
Icon( Icon(
@@ -894,18 +896,18 @@ fun ModuleItem(
Icon( Icon(
modifier = Modifier.size(20.dp).rotate(180f), modifier = Modifier.size(20.dp).rotate(180f),
imageVector = Icons.Outlined.Refresh, imageVector = Icons.Outlined.Refresh,
contentDescription = null, contentDescription = null
)
}
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
Text(
modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
) )
} }
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
//Text(
// modifier = Modifier.padding(start = 7.dp),
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
//)
//}
} }
} }
} }
@@ -932,4 +934,3 @@ fun ModuleItemPreview() {
) )
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
} }

View File

@@ -6,14 +6,16 @@ import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -24,13 +26,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.edit import androidx.core.content.edit
@@ -42,7 +42,6 @@ import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplat
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -52,25 +51,21 @@ import com.sukisu.ultra.R
import com.sukisu.ultra.* import com.sukisu.ultra.*
import com.sukisu.ultra.ui.component.* import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.* import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.util.LocalSnackbarHost import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid
/**
* @author weishu
* @date 2023/1/1.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
fun SettingScreen(navigator: DestinationsNavigator) { fun SettingScreen(navigator: DestinationsNavigator) {
// region 界面基础设置
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current val snackBarHost = LocalSnackbarHost.current
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
// endregion
Scaffold( Scaffold(
topBar = { topBar = {
@@ -114,55 +109,97 @@ fun SettingScreen(navigator: DestinationsNavigator) {
snackBarHost.showSnackbar(context.getString(R.string.log_saved)) snackBarHost.showSnackbar(context.getString(R.string.log_saved))
} }
} }
// region 配置项列表
// 配置
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) val profileTemplate = stringResource(id = R.string.settings_profile_template)
if (ksuIsValid) { KsuIsValid {
ListItem( SettingItem(
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) }, icon = Icons.Filled.Fence,
headlineContent = { Text(profileTemplate) }, title = profileTemplate,
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) }, summary = stringResource(id = R.string.settings_profile_template_summary),
modifier = Modifier.clickable { onClick = {
navigator.navigate(AppProfileTemplateScreenDestination) navigator.navigate(AppProfileTemplateScreenDestination)
} }
) )
} }
// 卸载模块开关 // 卸载模块开关
var umountChecked by rememberSaveable { var umountChecked by rememberSaveable {
mutableStateOf(Natives.isDefaultUmountModules()) mutableStateOf(Natives.isDefaultUmountModules())
} }
if (ksuIsValid) { KsuIsValid {
SwitchItem( SwitchSettingItem(
icon = Icons.Filled.FolderDelete, icon = Icons.Filled.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default), title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary), summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked checked = umountChecked,
) { onCheckedChange = {
if (Natives.setDefaultUmountModules(it)) { if (Natives.setDefaultUmountModules(it)) {
umountChecked = it umountChecked = it
} }
} }
)
} }
// SU 禁用开关(仅在兼容版本显示) // SU 禁用开关(仅在兼容版本显示)
if (ksuIsValid) { KsuIsValid {
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable { var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled()) mutableStateOf(!Natives.isSuEnabled())
} }
SwitchItem( SwitchSettingItem(
icon = Icons.Filled.RemoveModerator, icon = Icons.Filled.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su), title = stringResource(id = R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary), summary = stringResource(id = R.string.settings_disable_su_summary),
checked = isSuDisabled, checked = isSuDisabled,
) { checked -> onCheckedChange = { checked ->
val shouldEnable = !checked val shouldEnable = !checked
if (Natives.setSuEnabled(shouldEnable)) { if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = !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) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -172,15 +209,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
prefs.getBoolean("check_update", true) prefs.getBoolean("check_update", true)
) )
} }
SwitchItem( SwitchSettingItem(
icon = Icons.Filled.Update, icon = Icons.Filled.Update,
title = stringResource(id = R.string.settings_check_update), title = stringResource(id = R.string.settings_check_update),
summary = stringResource(id = R.string.settings_check_update_summary), summary = stringResource(id = R.string.settings_check_update_summary),
checked = checkUpdate checked = checkUpdate,
) { onCheckedChange = {
prefs.edit {putBoolean("check_update", it) } prefs.edit {putBoolean("check_update", it) }
checkUpdate = it checkUpdate = it
} }
)
// Web调试开关 // Web调试开关
var enableWebDebugging by rememberSaveable { var enableWebDebugging by rememberSaveable {
@@ -188,91 +226,85 @@ fun SettingScreen(navigator: DestinationsNavigator) {
prefs.getBoolean("enable_web_debugging", false) prefs.getBoolean("enable_web_debugging", false)
) )
} }
if (Natives.isKsuValid(ksuApp.packageName)) { KsuIsValid {
SwitchItem( SwitchSettingItem(
icon = Icons.Filled.DeveloperMode, icon = Icons.Filled.DeveloperMode,
title = stringResource(id = R.string.enable_web_debugging), title = stringResource(id = R.string.enable_web_debugging),
summary = stringResource(id = R.string.enable_web_debugging_summary), summary = stringResource(id = R.string.enable_web_debugging_summary),
checked = enableWebDebugging checked = enableWebDebugging,
) { onCheckedChange = {
prefs.edit { putBoolean("enable_web_debugging", it) } prefs.edit { putBoolean("enable_web_debugging", it) }
enableWebDebugging = 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 { SettingItem(
icon = Icons.Filled.Settings,
title = stringResource(id = R.string.more_settings),
summary = stringResource(id = R.string.more_settings),
onClick = {
navigator.navigate(MoreSettingsScreenDestination) navigator.navigate(MoreSettingsScreenDestination)
} }
) )
}
}
// 设置分组卡片 - 工具
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.tools),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
var showBottomsheet by remember { mutableStateOf(false) } var showBottomsheet by remember { mutableStateOf(false) }
ListItem( SettingItem(
leadingContent = { icon = Icons.Filled.BugReport,
Icon( title = stringResource(id = R.string.send_log),
Icons.Filled.BugReport, onClick = {
stringResource(id = R.string.send_log)
)
},
headlineContent = { Text(stringResource(id = R.string.send_log)) },
modifier = Modifier.clickable {
showBottomsheet = true showBottomsheet = true
} }
) )
if (showBottomsheet) { if (showBottomsheet) {
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showBottomsheet = false }, onDismissRequest = { showBottomsheet = false },
content = { containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(10.dp) .fillMaxWidth()
.align(Alignment.CenterHorizontally) .padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
Box { LogActionButton(
Column( icon = Icons.Filled.Save,
modifier = Modifier text = stringResource(R.string.save_log),
.padding(16.dp) onClick = {
.clickable {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter) val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
showBottomsheet = false showBottomsheet = false
} }
) {
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
)
}
) LogActionButton(
} icon = Icons.Filled.Share,
} text = stringResource(R.string.send_log),
Box { onClick = {
Column(
modifier = Modifier
.padding(16.dp)
.clickable {
scope.launch { scope.launch {
val bugreport = loadingDialog.withLoading { val bugreport = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -299,30 +331,15 @@ fun SettingScreen(navigator: DestinationsNavigator) {
context.getString(R.string.send_log) context.getString(R.string.send_log)
) )
) )
}
} showBottomsheet = false
) {
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
)
}
)
}
}
} }
} }
) )
} }
Spacer(modifier = Modifier.height(16.dp))
}
}
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) { if (lkmMode) {
@@ -330,22 +347,176 @@ fun SettingScreen(navigator: DestinationsNavigator) {
loadingDialog.withLoading(it) loadingDialog.withLoading(it)
} }
} }
}
}
val about = stringResource(id = R.string.about) // 设置分组卡片 - 关于
ListItem( Card(
leadingContent = { modifier = Modifier
Icon( .fillMaxWidth()
Icons.Filled.ContactPage, .padding(horizontal = 16.dp, vertical = 8.dp),
about 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)
) )
},
headlineContent = { Text(about) }, SettingItem(
modifier = Modifier.clickable { icon = Icons.Filled.Info,
title = stringResource(R.string.about),
onClick = {
aboutDialog.show() 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 @Composable
@@ -381,16 +552,11 @@ fun UninstallItem(
} }
} }
} }
val uninstall = stringResource(id = R.string.settings_uninstall)
ListItem( SettingItem(
leadingContent = { icon = Icons.Filled.Delete,
Icon( title = stringResource(id = R.string.settings_uninstall),
Icons.Filled.Delete, onClick = {
uninstall
)
},
headlineContent = { Text(uninstall) },
modifier = Modifier.clickable {
uninstallDialog.show() uninstallDialog.show()
} }
) )
@@ -436,7 +602,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
val cardColor = if (!ThemeConfig.useDynamicColor) { val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast ThemeConfig.currentTheme.ButtonContrast
} else { } else {
MaterialTheme.colorScheme.secondaryContainer MaterialTheme.colorScheme.surfaceContainerHigh
} }
AlertDialog( AlertDialog(
@@ -444,29 +610,46 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
dismiss() dismiss()
}, },
title = { title = {
Text(text = stringResource(R.string.settings_uninstall)) Text(
text = stringResource(R.string.settings_uninstall),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
}, },
text = { text = {
Column { Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
listOptions.forEachIndexed { index, option -> listOptions.forEachIndexed { index, option ->
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable { .clickable {
selection = options[index] selection = options[index]
} }
.padding(vertical = 8.dp) .padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = options[index].icon, imageVector = options[index].icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(end = 8.dp) tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
) )
Column { Column {
Text(text = option.titleText) Text(
text = option.titleText,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
option.subtitleText?.let { option.subtitleText?.let {
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@@ -476,7 +659,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
} }
}, },
confirmButton = { confirmButton = {
androidx.compose.material3.TextButton( TextButton(
onClick = { onClick = {
if (selection != UninstallType.NONE) { if (selection != UninstallType.NONE) {
onSelected(selection) onSelected(selection)
@@ -484,21 +667,27 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
dismiss() dismiss()
} }
) { ) {
Text(text = stringResource(android.R.string.ok)) Text(
text = stringResource(android.R.string.ok),
color = MaterialTheme.colorScheme.primary
)
} }
}, },
dismissButton = { dismissButton = {
androidx.compose.material3.TextButton( TextButton(
onClick = { onClick = {
dismiss() dismiss()
} }
) { ) {
Text(text = stringResource(android.R.string.cancel)) Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
} }
}, },
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f), containerColor = cardColor,
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.extraLarge,
tonalElevation = getCardElevation() tonalElevation = 4.dp
) )
} }
} }
@@ -508,24 +697,26 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
private fun TopBar( private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.secondaryContainer val systemIsDark = isSystemInDarkTheme()
val cardAlpha = CardConfig.cardAlpha val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
cardAlpha
} else {
if (systemIsDark) 0.8f else 1f
}
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.settings)) }, title = {
Text(
text = stringResource(R.string.settings),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha), containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha) scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
), ),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }
@Preview
@Composable
private fun SettingsPreview() {
SettingScreen(EmptyDestinationsNavigator)
}

View File

@@ -1,6 +1,8 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -13,7 +15,10 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@@ -21,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -34,6 +40,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.component.SearchAppBar import com.sukisu.ultra.ui.component.SearchAppBar
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.util.ModuleModify import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
@@ -80,21 +87,29 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
) { ) {
Icon( Icon(
imageVector = Icons.Filled.MoreVert, imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings) contentDescription = stringResource(id = R.string.settings),
) )
DropdownMenu(expanded = showDropdown, onDismissRequest = { DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false showDropdown = false
}) { }) {
DropdownMenuItem(text = { DropdownMenuItem(
Text(stringResource(R.string.refresh)) text = { Text(stringResource(R.string.refresh)) },
}, onClick = { leadingIcon = {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
)
},
onClick = {
scope.launch { scope.launch {
viewModel.fetchAppList() viewModel.fetchAppList()
} }
showDropdown = false showDropdown = false
}) }
DropdownMenuItem(text = { )
DropdownMenuItem(
text = {
Text( Text(
if (viewModel.showSystemApps) { if (viewModel.showSystemApps) {
stringResource(R.string.hide_system_apps) stringResource(R.string.hide_system_apps)
@@ -102,22 +117,46 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
stringResource(R.string.show_system_apps) stringResource(R.string.show_system_apps)
} }
) )
}, onClick = { },
leadingIcon = {
Icon(
imageVector = if (viewModel.showSystemApps)
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
contentDescription = null,
)
},
onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false showDropdown = false
}) }
DropdownMenuItem(text = { )
Text(stringResource(R.string.backup_allowlist)) HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
}, onClick = { DropdownMenuItem(
text = { Text(stringResource(R.string.backup_allowlist)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = null,
)
},
onClick = {
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
showDropdown = false showDropdown = false
}) }
DropdownMenuItem(text = { )
Text(stringResource(R.string.restore_allowlist)) DropdownMenuItem(
}, onClick = { text = { Text(stringResource(R.string.restore_allowlist)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.RestoreFromTrash,
contentDescription = null,
)
},
onClick = {
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
showDropdown = false showDropdown = false
}) }
)
} }
} }
}, },
@@ -127,22 +166,56 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
snackbarHost = { SnackbarHost(snackBarHostState) }, snackbarHost = { SnackbarHost(snackBarHostState) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
bottomBar = { bottomBar = {
// 批量操作按钮,直接放在底部栏 AnimatedVisibility(
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
tonalElevation = cardElevation,
shadowElevation = cardElevation
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp), .padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly 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( Button(
onClick = { onClick = {
scope.launch { scope.launch {
viewModel.updateBatchPermissions(true) 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)) Text(stringResource(R.string.batch_authorization))
} }
@@ -151,13 +224,23 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
scope.launch { scope.launch {
viewModel.updateBatchPermissions(false) 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)) Text(stringResource(R.string.batch_cancel_authorization))
} }
} }
} }
} }
}
) { innerPadding -> ) { innerPadding ->
PullToRefreshBox( PullToRefreshBox(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
@@ -170,15 +253,23 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = PaddingValues(
top = 8.dp,
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
)
) { ) {
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中 // 获取分组后的应用列表
val rootApps = viewModel.appList.filter { it.allowSu } val rootApps = viewModel.appList.filter { it.allowSu }
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile } val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile } val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
// 显示ROOT权限应用组 // 显示ROOT权限应用组
if (rootApps.isNotEmpty()) { if (rootApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.apps_with_root))
}
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app -> items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
AppItem( AppItem(
app = app, app = app,
@@ -214,6 +305,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
// 显示自定义配置应用组 // 显示自定义配置应用组
if (customApps.isNotEmpty()) { if (customApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
}
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app -> items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
AppItem( AppItem(
app = app, app = app,
@@ -249,6 +344,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
// 显示其他应用组 // 显示其他应用组
if (otherApps.isNotEmpty()) { if (otherApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.other_apps))
}
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app -> items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
AppItem( AppItem(
app = app, app = app,
@@ -281,6 +380,38 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
) )
} }
} }
// 当没有应用显示时显示空状态
if (viewModel.appList.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Apps,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
text = stringResource(R.string.no_apps_found),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
} }
} }
} }
@@ -291,7 +422,7 @@ fun GroupHeader(title: String) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
Text( Text(
@@ -299,7 +430,7 @@ fun GroupHeader(title: String) {
style = TextStyle( style = TextStyle(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.primary
) )
) )
} }
@@ -316,33 +447,48 @@ private fun AppItem(
onLongClick: () -> Unit, onLongClick: () -> Unit,
viewModel: SuperUserViewModel viewModel: SuperUserViewModel
) { ) {
ListItem( 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 modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = 0.dp,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
.then(
if (isSelected)
Modifier.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.medium
)
else
Modifier
)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongClick() }, onLongPress = { onLongClick() },
onTap = { onClick() } onTap = { onClick() }
) )
},
headlineContent = { Text(app.label) },
supportingContent = {
Column {
Text(app.packageName)
FlowRow {
if (app.allowSu) {
LabelText(label = "ROOT")
} else {
if (Natives.uidShouldUmount(app.uid)) {
LabelText(label = "UMOUNT")
} }
} ) {
if (app.hasCustomProfile) { Row(
LabelText(label = "CUSTOM") modifier = Modifier
} .fillMaxWidth()
} .padding(16.dp),
} verticalAlignment = Alignment.CenterVertically
}, ) {
leadingContent = {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
.data(app.packageInfo) .data(app.packageInfo)
@@ -350,43 +496,93 @@ private fun AppItem(
.build(), .build(),
contentDescription = app.label, contentDescription = app.label,
modifier = Modifier modifier = Modifier
.padding(4.dp) .padding(end = 16.dp)
.width(48.dp) .size(48.dp)
.height(48.dp) .clip(MaterialTheme.shapes.small)
) )
},
trailingContent = { Column(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
Text(
text = app.label,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
Text(
text = app.packageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
FlowRow(
modifier = Modifier.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (app.allowSu) {
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) { if (!viewModel.showBatchActions) {
Switch( Switch(
checked = app.allowSu, checked = app.allowSu,
onCheckedChange = onSwitchChange 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 { } else {
Checkbox( Checkbox(
checked = isSelected, checked = isSelected,
onCheckedChange = { onToggleSelection() } onCheckedChange = { onToggleSelection() },
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
) )
} }
} }
) }
} }
@Composable @Composable
fun LabelText(label: String) { fun LabelText(label: String, backgroundColor: Color) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 4.dp, end = 4.dp) .padding(top = 2.dp, end = 2.dp)
.background( .background(
Color.Black, backgroundColor,
shape = RoundedCornerShape(4.dp) shape = RoundedCornerShape(4.dp)
) )
.clip(RoundedCornerShape(4.dp))
) { ) {
Text( Text(
text = label, text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
style = TextStyle( style = TextStyle(
fontSize = 8.sp, fontSize = 10.sp,
color = Color.White, color = Color.White,
fontWeight = FontWeight.Medium
) )
) )
} }

View File

@@ -33,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
@@ -61,7 +62,7 @@ import com.ramcosta.composedestinations.result.getOr
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
/** /**
@@ -79,11 +80,6 @@ fun AppProfileTemplateScreen(
val viewModel = viewModel<TemplateViewModel>() val viewModel = viewModel<TemplateViewModel>()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (viewModel.templateList.isEmpty()) { if (viewModel.templateList.isEmpty()) {
@@ -98,6 +94,9 @@ fun AppProfileTemplateScreen(
} }
} }
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
Scaffold( Scaffold(
topBar = { topBar = {
val context = LocalContext.current val context = LocalContext.current
@@ -109,6 +108,10 @@ fun AppProfileTemplateScreen(
} }
TopBar( TopBar(
onBack = dropUnlessResumed { navigator.popBackStack() }, onBack = dropUnlessResumed { navigator.popBackStack() },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColorUse.copy(alpha = cardAlpha),
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
),
onSync = { onSync = {
scope.launch { viewModel.fetchTemplates(true) } scope.launch { viewModel.fetchTemplates(true) }
}, },
@@ -155,7 +158,6 @@ fun AppProfileTemplateScreen(
}, },
icon = { Icon(Icons.Filled.Add, null) }, icon = { Icon(Icons.Filled.Add, null) },
text = { Text(stringResource(id = R.string.app_profile_template_create)) }, text = { Text(stringResource(id = R.string.app_profile_template_create)) },
containerColor = cardColor.copy(alpha = 1f),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) )
}, },
@@ -205,17 +207,17 @@ private fun TemplateItem(
) )
Text(template.description) Text(template.description)
FlowRow { FlowRow {
LabelText(label = "UID: ${template.uid}") LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
LabelText(label = "GID: ${template.gid}") LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
LabelText(label = template.context) LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
if (template.local) { if (template.local) {
LabelText(label = "local") LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
} else { } else {
LabelText(label = "remote") LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
}
} }
} }
} }
},
) )
} }
@@ -226,12 +228,20 @@ private fun TopBar(
onSync: () -> Unit = {}, onSync: () -> Unit = {},
onImport: () -> Unit = {}, onImport: () -> Unit = {},
onExport: () -> Unit = {}, onExport: () -> Unit = {},
colors: TopAppBarColors,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
TopAppBar( TopAppBar(
title = { title = {
Text(stringResource(R.string.settings_profile_template)) Text(stringResource(R.string.settings_profile_template))
}, },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = onBack onClick = onBack

View File

@@ -13,76 +13,95 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
object CardConfig { object CardConfig {
val defaultElevation: Dp = 0.dp val settingElevation: Dp = 4.dp
val customBackgroundElevation: Dp = 0.dp
var cardAlpha by mutableStateOf(0.45f) var cardAlpha by mutableStateOf(1f)
var cardElevation by mutableStateOf(defaultElevation) var cardElevation by mutableStateOf(settingElevation)
var isShadowEnabled by mutableStateOf(true) var isShadowEnabled by mutableStateOf(true)
var isCustomAlphaSet by mutableStateOf(false) var isCustomAlphaSet by mutableStateOf(false)
var isUserDarkModeEnabled by mutableStateOf(false) var isUserDarkModeEnabled by mutableStateOf(false)
var isUserLightModeEnabled by mutableStateOf(false) var isUserLightModeEnabled by mutableStateOf(false)
var isCustomBackgroundEnabled by mutableStateOf(false) var isCustomBackgroundEnabled by mutableStateOf(false)
/**
* 保存卡片配置到SharedPreferences
*/
fun save(context: Context) { fun save(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().apply { prefs.edit().apply {
putFloat("card_alpha", cardAlpha) putFloat("card_alpha", cardAlpha)
putBoolean("custom_background_enabled", cardElevation == 0.dp) putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
putBoolean("is_shadow_enabled", isShadowEnabled)
putBoolean("is_custom_alpha_set", isCustomAlphaSet) putBoolean("is_custom_alpha_set", isCustomAlphaSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
apply() apply()
} }
} }
/**
* 从SharedPreferences加载卡片配置
*/
fun load(context: Context) { fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 0.45f) cardAlpha = prefs.getFloat("card_alpha", 1f)
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false) updateShadowEnabled(isShadowEnabled)
} }
/**
* 更新阴影启用状态
*/
fun updateShadowEnabled(enabled: Boolean) { fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled isShadowEnabled = enabled
cardElevation = if (enabled) defaultElevation else 0.dp cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
customBackgroundElevation
} else if (enabled) {
settingElevation
} else {
customBackgroundElevation
}
} }
/**
* 设置深色模式默认值
*/
fun setDarkModeDefaults() { fun setDarkModeDefaults() {
if (!isCustomAlphaSet) { if (!isCustomAlphaSet) {
cardAlpha = 0.35f cardAlpha = 1f
cardElevation = 0.dp
} }
updateShadowEnabled(isShadowEnabled)
} }
} }
/**
* 获取卡片颜色配置
*/
@Composable @Composable
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors( fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = when { contentColor = determineContentColor(originalColor)
CardConfig.isUserLightModeEnabled -> {
Color.Black
}
CardConfig.isUserDarkModeEnabled -> {
Color.White
}
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
Color.Black
}
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
Color.Black
}
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
Color.White
}
else -> {
Color.White
}
}
) )
fun getCardElevation() = CardConfig.cardElevation /**
* 根据背景颜色、主题模式和用户设置确定内容颜色
*/
@Composable
private fun determineContentColor(originalColor: Color): Color {
val isDarkTheme = isSystemInDarkTheme()
if (ThemeConfig.isThemeChanging) {
return if (isDarkTheme) Color.White else Color.Black
}
return when {
CardConfig.isUserLightModeEnabled -> Color.Black
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
isDarkTheme -> Color.White
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
}
}

View File

@@ -17,153 +17,256 @@ sealed class ThemeColors {
abstract val OnTertiaryContainer: Color abstract val OnTertiaryContainer: Color
abstract val ButtonContrast: Color abstract val ButtonContrast: Color
open fun getCustomSliderActiveColor(): Color = Primary // 表面颜色
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer abstract val Surface: Color
abstract val SurfaceVariant: Color
abstract val OnSurface: Color
abstract val OnSurfaceVariant: Color
// Default Theme (white) // 错误状态颜色
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() { 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 Primary = Color(0xFF2196F3)
override val Secondary = Color(0xFF1E88E5) override val Secondary = Color(0xFF64B5F6)
override val Tertiary = Color(0xFF0D47A1) override val Tertiary = Color(0xFF0D47A1)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFCBE6FC) override val PrimaryContainer = Color(0xFFD6EAFF)
override val SecondaryContainer = Color(0xFFBBDEFB) override val SecondaryContainer = Color(0xFFE3F2FD)
override val TertiaryContainer = Color(0xFF90CAF9) override val TertiaryContainer = Color(0xFFCFD8DC)
override val OnPrimaryContainer = Color(0xFF0A1A2E) override val OnPrimaryContainer = Color(0xFF0A3049)
override val OnSecondaryContainer = Color(0xFF0A192D) override val OnSecondaryContainer = Color(0xFF0D3C61)
override val OnTertiaryContainer = Color(0xFF071B3D) override val OnTertiaryContainer = Color(0xFF071D41)
override val ButtonContrast = Color(0xFF00BFFF) 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)
} }
// Green Theme // 绿色主题
object Green : ThemeColors() { object Green : ThemeColors() {
override val Primary = Color(0xFF4CAF50) override val Primary = Color(0xFF43A047)
override val Secondary = Color(0xFF43A047) override val Secondary = Color(0xFF66BB6A)
override val Tertiary = Color(0xFF1B5E20) override val Tertiary = Color(0xFF1B5E20)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFC8E6C9) override val PrimaryContainer = Color(0xFFD8EFDB)
override val SecondaryContainer = Color(0xFFA5D6A7) override val SecondaryContainer = Color(0xFFE8F5E9)
override val TertiaryContainer = Color(0xFF81C784) override val TertiaryContainer = Color(0xFFB9F6CA)
override val OnPrimaryContainer = Color(0xFF0A1F0B) override val OnPrimaryContainer = Color(0xFF0A280D)
override val OnSecondaryContainer = Color(0xFF0A1D0B) override val OnSecondaryContainer = Color(0xFF0E2912)
override val OnTertiaryContainer = Color(0xFF071F09) override val OnTertiaryContainer = Color(0xFF051B07)
override val ButtonContrast = Color(0xFF32CD32) 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)
} }
// Purple Theme // 紫色主题
object Purple : ThemeColors() { object Purple : ThemeColors() {
override val Primary = Color(0xFF9C27B0) override val Primary = Color(0xFF9C27B0)
override val Secondary = Color(0xFF8E24AA) override val Secondary = Color(0xFFBA68C8)
override val Tertiary = Color(0xFF4A148C) override val Tertiary = Color(0xFF6A1B9A)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFE1BEE7) override val PrimaryContainer = Color(0xFFF3D8F8)
override val SecondaryContainer = Color(0xFFCE93D8) override val SecondaryContainer = Color(0xFFF5E9F7)
override val TertiaryContainer = Color(0xFFB39DDB) override val TertiaryContainer = Color(0xFFE1BEE7)
override val OnPrimaryContainer = Color(0xFF1F0A23) override val OnPrimaryContainer = Color(0xFF2A0934)
override val OnSecondaryContainer = Color(0xFF1C0A21) override val OnSecondaryContainer = Color(0xFF3C0F50)
override val OnTertiaryContainer = Color(0xFF12071C) override val OnTertiaryContainer = Color(0xFF1D0830)
override val ButtonContrast = Color(0xFFDA70D6) 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)
} }
// Orange Theme // 橙色主题
object Orange : ThemeColors() { object Orange : ThemeColors() {
override val Primary = Color(0xFFFF9800) override val Primary = Color(0xFFFF9800)
override val Secondary = Color(0xFFFB8C00) override val Secondary = Color(0xFFFFB74D)
override val Tertiary = Color(0xFFE65100) override val Tertiary = Color(0xFFE65100)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFF000000)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFE0B2) override val PrimaryContainer = Color(0xFFFFECCC)
override val SecondaryContainer = Color(0xFFFFCC80) override val SecondaryContainer = Color(0xFFFFF0D9)
override val TertiaryContainer = Color(0xFFFFB74D) override val TertiaryContainer = Color(0xFFFFD180)
override val OnPrimaryContainer = Color(0xFF1A1100) override val OnPrimaryContainer = Color(0xFF351F00)
override val OnSecondaryContainer = Color(0xFF1A1000) override val OnSecondaryContainer = Color(0xFF3D2800)
override val OnTertiaryContainer = Color(0xFF1A0B00) override val OnTertiaryContainer = Color(0xFF2E1500)
override val ButtonContrast = Color(0xFFFF6347) 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)
} }
// Pink Theme // 粉色主题
object Pink : ThemeColors() { object Pink : ThemeColors() {
override val Primary = Color(0xFFE91E63) override val Primary = Color(0xFFE91E63)
override val Secondary = Color(0xFFD81B60) override val Secondary = Color(0xFFF06292)
override val Tertiary = Color(0xFF880E4F) override val Tertiary = Color(0xFF880E4F)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFF8BBD0) override val PrimaryContainer = Color(0xFFFCE4EC)
override val SecondaryContainer = Color(0xFFF48FB1) override val SecondaryContainer = Color(0xFFFCE4EC)
override val TertiaryContainer = Color(0xFFE91E63) override val TertiaryContainer = Color(0xFFF8BBD0)
override val OnPrimaryContainer = Color(0xFF2E0A14) override val OnPrimaryContainer = Color(0xFF3B0819)
override val OnSecondaryContainer = Color(0xFF2B0A13) override val OnSecondaryContainer = Color(0xFF3B0819)
override val OnTertiaryContainer = Color(0xFF1C0311) override val OnTertiaryContainer = Color(0xFF2B0516)
override val ButtonContrast = Color(0xFFFF1493) 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)
} }
// Gray Theme // 灰色主题
object Gray : ThemeColors() { object Gray : ThemeColors() {
override val Primary = Color(0xFF9E9E9E) override val Primary = Color(0xFF607D8B)
override val Secondary = Color(0xFF757575) override val Secondary = Color(0xFF90A4AE)
override val Tertiary = Color(0xFF616161) override val Tertiary = Color(0xFF455A64)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFEEEEEE) override val PrimaryContainer = Color(0xFFECEFF1)
override val SecondaryContainer = Color(0xFFE0E0E0) override val SecondaryContainer = Color(0xFFECEFF1)
override val TertiaryContainer = Color(0xFFBDBDBD) override val TertiaryContainer = Color(0xFFCFD8DC)
override val OnPrimaryContainer = Color(0xFF1A1A1A) override val OnPrimaryContainer = Color(0xFF1A2327)
override val OnSecondaryContainer = Color(0xFF171717) override val OnSecondaryContainer = Color(0xFF1A2327)
override val OnTertiaryContainer = Color(0xFF141414) override val OnTertiaryContainer = Color(0xFF121A1D)
override val ButtonContrast = Color(0xFF696969) 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() { object Yellow : ThemeColors() {
override val Primary = Color(0xFFFFD700) override val Primary = Color(0xFFFFC107)
override val Secondary = Color(0xFFFFBC52) override val Secondary = Color(0xFFFFD54F)
override val Tertiary = Color(0xFF795548) override val Tertiary = Color(0xFFFF8F00)
override val OnPrimary = Color(0xFFFFFFFF) override val OnPrimary = Color(0xFF000000)
override val OnSecondary = Color(0xFFFFFFFF) override val OnSecondary = Color(0xFF000000)
override val OnTertiary = Color(0xFFFFFFFF) override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFF7D6) override val PrimaryContainer = Color(0xFFFFF8E1)
override val SecondaryContainer = Color(0xFFFFE6B3) override val SecondaryContainer = Color(0xFFFFF8E1)
override val TertiaryContainer = Color(0xFFD7CCC8) override val TertiaryContainer = Color(0xFFFFECB3)
override val OnPrimaryContainer = Color(0xFF1A1600) override val OnPrimaryContainer = Color(0xFF332A00)
override val OnSecondaryContainer = Color(0xFF1A1100) override val OnSecondaryContainer = Color(0xFF332A00)
override val OnTertiaryContainer = Color(0xFF1A1717) override val OnTertiaryContainer = Color(0xFF221200)
override val ButtonContrast = Color(0xFFFFD700) 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 { companion object {
fun fromName(name: String): ThemeColors = when (name.lowercase()) { fun fromName(name: String): ThemeColors = when (name.lowercase()) {
"blue" -> Blue
"green" -> Green "green" -> Green
"purple" -> Purple "purple" -> Purple
"orange" -> Orange "orange" -> Orange
"pink" -> Pink "pink" -> Pink
"gray" -> Gray "gray" -> Gray
"white" -> Yellow "yellow" -> Yellow
else -> Default else -> Default
} }
} }

View File

@@ -5,6 +5,10 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -14,19 +18,24 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.paint import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.ui.graphics.luminance import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -36,39 +45,286 @@ import androidx.core.net.toUri
import com.sukisu.ultra.ui.util.BackgroundTransformation import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground import com.sukisu.ultra.ui.util.saveTransformedBackground
/**
* 主题配置对象,管理应用的主题相关状态
*/
object ThemeConfig { object ThemeConfig {
var customBackgroundUri by mutableStateOf<Uri?>(null) var customBackgroundUri by mutableStateOf<Uri?>(null)
var forceDarkMode by mutableStateOf<Boolean?>(null) var forceDarkMode by mutableStateOf<Boolean?>(null)
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default) var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
var useDynamicColor by mutableStateOf(false) var useDynamicColor by mutableStateOf(false)
var backgroundImageLoaded by mutableStateOf(false)
var needsResetOnThemeChange by mutableStateOf(false)
var isThemeChanging by mutableStateOf(false)
var preventBackgroundRefresh by mutableStateOf(false)
private var lastDarkModeState: Boolean? = null
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
lastDarkModeState = currentDarkMode
return isChanged
}
fun resetBackgroundState() {
if (!preventBackgroundRefresh) {
backgroundImageLoaded = false
}
isThemeChanging = true
}
} }
/**
* 应用主题
*/
@Composable @Composable
private fun getDarkColorScheme() = darkColorScheme( fun KernelSUTheme(
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f), darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f), true -> true
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f), false -> false
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f), null -> isSystemInDarkTheme()
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f), },
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f), dynamicColor: Boolean = ThemeConfig.useDynamicColor,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f), content: @Composable () -> Unit
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f), ) {
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f), val context = LocalContext.current
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f), val systemIsDark = isSystemInDarkTheme()
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f), // 检测系统主题变化并保存状态
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, background = Color.Transparent,
surface = Color.Transparent, surface = Color.Transparent,
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f), onBackground = Color.White,
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f), 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), surfaceVariant = Color(0xFF2F2F2F),
onSurfaceVariant = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f), onSurfaceVariant = Color.White.copy(alpha = 0.7f),
outline = Color.White.copy(alpha = 0.12f), outline = Color.White.copy(alpha = 0.12f),
outlineVariant = 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 @Composable
private fun getLightColorScheme() = lightColorScheme( private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.Primary, primary = ThemeConfig.currentTheme.Primary,
onPrimary = ThemeConfig.currentTheme.OnPrimary, onPrimary = ThemeConfig.currentTheme.OnPrimary,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer, primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
@@ -88,163 +344,52 @@ private fun getLightColorScheme() = lightColorScheme(
surfaceVariant = Color(0xFFF5F5F5), surfaceVariant = Color(0xFFF5F5F5),
onSurfaceVariant = Color.Black.copy(alpha = 0.78f), onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
outline = Color.Black.copy(alpha = 0.12f), outline = Color.Black.copy(alpha = 0.12f),
outlineVariant = 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
) )
@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()
}
}
}
}
// 复制图片到应用内部存储
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? { private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
try { return try {
val contentResolver: ContentResolver = contentResolver val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri)!! val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background.jpg" val fileName = "custom_background.jpg"
val file = File(filesDir, fileName) val file = File(filesDir, fileName)
val outputStream = FileOutputStream(file)
val backupFile = File(filesDir, "${fileName}.backup")
val outputStream = FileOutputStream(backupFile)
val buffer = ByteArray(4 * 1024) val buffer = ByteArray(4 * 1024)
var read: Int var read: Int
while (inputStream.read(buffer).also { read = it } != -1) { while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read) outputStream.write(buffer, 0, read)
} }
outputStream.flush() outputStream.flush()
outputStream.close() outputStream.close()
inputStream.close() inputStream.close()
return Uri.fromFile(file)
if (file.exists()) {
file.delete()
}
backupFile.renameTo(file)
Uri.fromFile(file)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ImageCopy", "Failed to copy image: ${e.message}") Log.e("ImageCopy", "复制图片失败: ${e.message}")
return null null
} }
} }
// 保存变换后的背景图片到应用内部存储并更新配置 /**
* 保存并应用自定义背景
*/
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
val finalUri = if (transformation != null) { val finalUri = if (transformation != null) {
saveTransformedBackground(uri, transformation) saveTransformedBackground(uri, transformation)
@@ -252,36 +397,73 @@ fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTra
copyImageToInternalStorage(uri) copyImageToInternalStorage(uri)
} }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { .edit {
putString("custom_background", finalUri?.toString()) putString("custom_background", finalUri?.toString())
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
} }
ThemeConfig.customBackgroundUri = finalUri ThemeConfig.customBackgroundUri = finalUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
CardConfig.cardElevation = 0.dp CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true CardConfig.isCustomBackgroundEnabled = true
} }
// 保存背景图片到应用内部存储并更新配置 /**
* 保存自定义背景
*/
fun Context.saveCustomBackground(uri: Uri?) { fun Context.saveCustomBackground(uri: Uri?) {
val newUri = uri?.let { copyImageToInternalStorage(it) } val newUri = uri?.let { copyImageToInternalStorage(it) }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { .edit {
putString("custom_background", newUri?.toString()) putString("custom_background", newUri?.toString())
if (uri == null) {
// 如果清除背景,也重置阻止刷新标志
putBoolean("prevent_background_refresh", false)
} else {
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
} }
}
ThemeConfig.customBackgroundUri = newUri ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
if (uri != null) { if (uri != null) {
CardConfig.cardElevation = 0.dp CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true CardConfig.isCustomBackgroundEnabled = true
} }
} }
/**
* 加载自定义背景
*/
fun Context.loadCustomBackground() { fun Context.loadCustomBackground() {
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null) .getString("custom_background", null)
ThemeConfig.customBackgroundUri = uriString?.toUri()
val newUri = uriString?.toUri()
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
}
} }
/**
* 保存主题模式
*/
fun Context.saveThemeMode(forceDark: Boolean?) { fun Context.saveThemeMode(forceDark: Boolean?) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { .edit {
@@ -294,52 +476,49 @@ fun Context.saveThemeMode(forceDark: Boolean?) {
) )
} }
ThemeConfig.forceDarkMode = forceDark ThemeConfig.forceDarkMode = forceDark
ThemeConfig.needsResetOnThemeChange = forceDark == null
} }
/**
* 加载主题模式
*/
fun Context.loadThemeMode() { fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system") .getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) { ThemeConfig.forceDarkMode = when(mode) {
"dark" -> true "dark" -> true
"light" -> false "light" -> false
else -> null else -> null
} }
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
} }
/**
* 保存主题颜色
*/
fun Context.saveThemeColors(themeName: String) { fun Context.saveThemeColors(themeName: String) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { .edit {
putString("theme_colors", themeName) putString("theme_colors", themeName)
} }
ThemeConfig.currentTheme = when(themeName) { ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
"blue" -> ThemeColors.Blue
"green" -> ThemeColors.Green
"purple" -> ThemeColors.Purple
"orange" -> ThemeColors.Orange
"pink" -> ThemeColors.Pink
"gray" -> ThemeColors.Gray
"yellow" -> ThemeColors.Yellow
else -> ThemeColors.Default
}
} }
/**
* 加载主题颜色
*/
fun Context.loadThemeColors() { fun Context.loadThemeColors() {
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_colors", "default") .getString("theme_colors", "default")
ThemeConfig.currentTheme = when(themeName) { ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
"blue" -> ThemeColors.Blue
"green" -> ThemeColors.Green
"purple" -> ThemeColors.Purple
"orange" -> ThemeColors.Orange
"pink" -> ThemeColors.Pink
"gray" -> ThemeColors.Gray
"yellow" -> ThemeColors.Yellow
else -> ThemeColors.Default
}
} }
/**
* 保存动态颜色状态
*/
fun Context.saveDynamicColorState(enabled: Boolean) { fun Context.saveDynamicColorState(enabled: Boolean) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { .edit {
@@ -348,28 +527,12 @@ fun Context.saveDynamicColorState(enabled: Boolean) {
ThemeConfig.useDynamicColor = enabled ThemeConfig.useDynamicColor = enabled
} }
/**
* 加载动态颜色状态
*/
fun Context.loadDynamicColorState() { fun Context.loadDynamicColorState() {
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", true) .getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled ThemeConfig.useDynamicColor = enabled
} }
private fun adjustColor(color: Color): Color {
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)
}

View File

@@ -1,33 +1,108 @@
package com.sukisu.ultra.ui.theme package com.sukisu.ultra.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with val Typography = Typography(
val Typography = androidx.compose.material3.Typography( // 大标题
bodyLarge = TextStyle( displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 57.sp,
lineHeight = 24.sp, lineHeight = 64.sp,
letterSpacing = 0.5.sp letterSpacing = (-0.25).sp
) ),
/* Other default text styles to override displayMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
// 标题
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// 标题栏
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontWeight = FontWeight.SemiBold,
fontWeight = FontWeight.Normal,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
titleMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
// 主体文字
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
// 标签
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontSize = 11.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
*/
) )

View File

@@ -63,10 +63,6 @@ fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: Backgrou
val safeScale = maxOf(0.1f, transformation.scale) val safeScale = maxOf(0.1f, transformation.scale)
matrix.postScale(safeScale, safeScale) matrix.postScale(safeScale, safeScale)
// 计算中心点
val centerX = targetWidth / 2f
val centerY = targetHeight / 2f
// 计算偏移量,确保不会出现负最大值的问题 // 计算偏移量,确保不会出现负最大值的问题
val widthDiff = (bitmap.width * safeScale - targetWidth) val widthDiff = (bitmap.width * safeScale - targetWidth)
val heightDiff = (bitmap.height * safeScale - targetHeight) val heightDiff = (bitmap.height * safeScale - targetHeight)

View File

@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
fun controlKpmModule(name: String, args: String? = null): Int { fun controlKpmModule(name: String, args: String? = null): Int {
val shell = getRootShell() val shell = getRootShell()
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}" val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
val result = runCmd(shell, cmd) val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1 return result.trim().toIntOrNull() ?: -1
} }

View File

@@ -68,9 +68,9 @@ class SuperUserViewModel : ViewModel() {
// 批量操作相关状态 // 批量操作相关状态
var showBatchActions by mutableStateOf(false) var showBatchActions by mutableStateOf(false)
private set internal set
var selectedApps by mutableStateOf<Set<String>>(emptySet()) var selectedApps by mutableStateOf<Set<String>>(emptySet())
private set internal set
private val sortedList by derivedStateOf { private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> { val comparator = compareBy<AppInfo> {

View File

@@ -73,8 +73,7 @@
<string name="require_kernel_version" formatted="false">現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください!</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">デフォルトでモジュールのマウントを解除する</string>
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string> <string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
<string name="settings_susfs_toggle">Kprobe フックを非表示にする</string> <string name="settings_susfs_toggle">kprobe フックを無効化</string>
<string name="settings_susfs_toggle_summary">KSU によって生成された Kprobe フックを無効化して、代替となる組み込みの非 Kprobe を有効化します。Kprobe をサポートしない 非 GKI カーネルに適用される同等の機能を実装します。</string>
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string> <string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
<string name="profile_selinux_domain">ドメイン</string> <string name="profile_selinux_domain">ドメイン</string>
<string name="profile_selinux_rules">ルール</string> <string name="profile_selinux_rules">ルール</string>
@@ -117,7 +116,7 @@
<string name="enable_web_debugging">WebView デバッグを有効化する</string> <string name="enable_web_debugging">WebView デバッグを有効化する</string>
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合でのみ有効化してください</string> <string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合でのみ有効化してください</string>
<string name="direct_install">直接インストール (推奨)</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">非アクティブなスロットにインストール (OTA 後)</string>
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。 <string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
\nこのオプションは、OTA が完了した後にのみ使用してください。 \nこのオプションは、OTA が完了した後にのみ使用してください。
@@ -171,8 +170,8 @@
<string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string> <string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string>
<string name="backup_allowlist">許可リストをバックアップ</string> <string name="backup_allowlist">許可リストをバックアップ</string>
<string name="restore_allowlist">許可リストを復元</string> <string name="restore_allowlist">許可リストを復元</string>
<string name="settings_custom_background">カスタム背景を設定</string> <string name="settings_custom_background">カスタムアプリ背景</string>
<string name="settings_custom_background_summary">カスタム背景を設定します</string> <string name="settings_custom_background_summary">背景にする画像を選択してください</string>
<string name="settings_card_alpha">ナビゲーションバーの透過</string> <string name="settings_card_alpha">ナビゲーションバーの透過</string>
<string name="settings_restore_default">デフォルトに復元</string> <string name="settings_restore_default">デフォルトに復元</string>
<string name="home_android_version">Android のバージョン</string> <string name="home_android_version">Android のバージョン</string>
@@ -195,16 +194,17 @@
<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">SuSFS ステータスを非表示にする</string>
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string> <string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
<string name="theme_mode">テーマモード</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_follow_system">システムに従う</string>
<string name="theme_light">ライトカラー</string> <string name="theme_light">ライト</string>
<string name="theme_dark">ダークカラー</string> <string name="theme_dark">ダーク</string>
<string name="manual_hook">手動でフック</string> <string name="manual_hook">手動でフック</string>
<string name="dynamic_color_title">ダイナミックカラー</string> <string name="dynamic_color_title">ダイナミックカラー</string>
<string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string> <string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string>
<string name="choose_theme_color">テーマカラーを選択</string> <string name="choose_theme_color">テーマカラーを選択</string>
<string name="color_default">ホワイト</string> <string name="color_default">ブルー</string>
<string name="color_blue">ブルー</string>
<string name="color_green">グリーン</string> <string name="color_green">グリーン</string>
<string name="color_purple">パープル</string> <string name="color_purple">パープル</string>
<string name="color_orange">オレンジ</string> <string name="color_orange">オレンジ</string>
@@ -213,8 +213,8 @@
<string name="color_ivory">アイボリー</string> <string name="color_ivory">アイボリー</string>
<string name="flash_option">ブラシの設定</string> <string name="flash_option">ブラシの設定</string>
<string name="flash_option_tip">フラッシュするファイルを選択</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="horizon_kernel_summary">AnyKernel3 カーネルファイルをフラッシュします</string>
<string name="root_required">root 権限が必要です</string> <string name="root_required">root 権限が必要です</string>
<string name="copy_failed">ファイルのコピーに失敗しました</string> <string name="copy_failed">ファイルのコピーに失敗しました</string>
<string name="reboot_complete_title">スクラブが完了しました</string> <string name="reboot_complete_title">スクラブが完了しました</string>
@@ -222,48 +222,107 @@
<string name="yes">はい</string> <string name="yes">はい</string>
<string name="no">いいえ</string> <string name="no">いいえ</string>
<string name="failed_reboot">再起動に失敗しました</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="batch_cancel_authorization">認証を一括でキャンセル</string>
<string name="backup">バックアップ</string> <string name="backup">バックアップ</string>
<string name="color_yellow">イエロー</string> <string name="color_yellow">イエロー</string>
<string name="kpm">KPM モジュール</string> <string name="kpm">カーネルモジュール</string>
<string name="kpm_title">カーネルモジュール</string> <string name="kpm_title">カーネルモジュール</string>
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string> <string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
<string name="kpm_version">リリース</string> <string name="kpm_version">バージョン</string>
<string name="kpm_author">作者</string> <string name="kpm_author">作者</string>
<string name="kpm_uninstall">アンインストール</string> <string name="kpm_uninstall">アンインストール</string>
<string name="kpm_uninstall_success">アンインストールに失敗しました</string> <string name="kpm_uninstall_success">アンインストールに失敗しました</string>
<string name="kpm_uninstall_failed">アンインストールに失敗しました</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_success">KPM モジュールの読み込みに成功しました</string>
<string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string> <string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string>
<string name="kpm_args">KPM パラメータ</string> <string name="kpm_args">パラメータ</string>
<string name="kpm_control">完全</string> <string name="kpm_control">実行</string>
<string name="home_kpm_version">KPM のバージョン</string> <string name="home_kpm_version">KPM のバージョン</string>
<string name="close_notice">閉じる</string> <string name="close_notice">閉じる</string>
<string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string> <string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string> <string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string>
<string name="kpm_control_success">成功</string> <string name="kpm_control_success">成功</string>
<string name="kpm_control_failed">失敗</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="not_supported">非対応</string>
<string name="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="kpm_invalid_file">無効な KPM ファイル</string>
<string name="kernel_patched">カーネルはパッチされていません</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="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_load">読み込む</string>
<string name="kpm_install_mode_embed">埋め込む</string> <string name="kpm_install_mode_embed">埋め込む</string>
<string name="kpm_install_mode_description">モジュール:%1\$s のインストールモードを選択してください \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="log_failed_to_check_module_file">モジュールファイルの存在を確認できませんでした</string>
<string name="snackbar_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_title">アンインストールを確認</string>
<string name="confirm_uninstall_confirm">アンインストール</string> <string name="confirm_uninstall_confirm">アンインストール</string>
<string name="confirm_uninstall_dismiss">中止</string> <string name="confirm_uninstall_dismiss">キャンセル</string>
<string name="theme_color">テーマカラー</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_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> </resources>

View File

@@ -201,8 +201,7 @@
<string name="dynamic_color_title">Màu sắc động</string> <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="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="choose_theme_color">Chọn màu chủ đề</string>
<string name="color_default">Trắng</string> <string name="color_default">Xanh dương</string>
<string name="color_blue">Xanh dương</string>
<string name="color_green">Xanh lá</string> <string name="color_green">Xanh lá</string>
<string name="color_purple">Tím</string> <string name="color_purple">Tím</string>
<string name="color_orange">Cam</string> <string name="color_orange">Cam</string>

View File

@@ -117,7 +117,7 @@
<string name="enable_web_debugging">启用 WebView 调试</string> <string name="enable_web_debugging">启用 WebView 调试</string>
<string name="enable_web_debugging_summary">可用于调试 WebUI 。请仅在需要时启用。</string> <string name="enable_web_debugging_summary">可用于调试 WebUI 。请仅在需要时启用。</string>
<string name="direct_install">直接安装(推荐)</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">安装到未使用的槽位OTA 后)</string>
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认</string> <string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认</string>
<string name="install_next">下一步</string> <string name="install_next">下一步</string>
@@ -172,7 +172,7 @@
<string name="restore_allowlist">还原应用列表</string> <string name="restore_allowlist">还原应用列表</string>
<string name="settings_custom_background">自定义背景</string> <string name="settings_custom_background">自定义背景</string>
<string name="settings_custom_background_summary">选择一张图片作为应用背景</string> <string name="settings_custom_background_summary">选择一张图片作为应用背景</string>
<string name="settings_card_alpha">卡片透明度</string> <string name="settings_card_alpha">卡片透明度</string>
<string name="settings_restore_default">恢复默认</string> <string name="settings_restore_default">恢复默认</string>
<string name="home_android_version">Android 版本</string> <string name="home_android_version">Android 版本</string>
<string name="home_device_model">设备</string> <string name="home_device_model">设备</string>
@@ -194,6 +194,8 @@
<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">隐藏 SuSFS 状态信息</string>
<string name="hide_susfs_status_summary">隐藏主页上的 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_mode">主题模式</string>
<string name="theme_follow_system">跟随系统</string> <string name="theme_follow_system">跟随系统</string>
<string name="theme_light">浅色</string> <string name="theme_light">浅色</string>
@@ -202,8 +204,7 @@
<string name="dynamic_color_title">动态颜色</string> <string name="dynamic_color_title">动态颜色</string>
<string name="dynamic_color_summary">使用系统主题的动态颜色</string> <string name="dynamic_color_summary">使用系统主题的动态颜色</string>
<string name="choose_theme_color">选择主题色</string> <string name="choose_theme_color">选择主题色</string>
<string name="color_default"></string> <string name="color_default"></string>
<string name="color_blue">蓝色</string>
<string name="color_green">绿色</string> <string name="color_green">绿色</string>
<string name="color_purple">紫色</string> <string name="color_purple">紫色</string>
<string name="color_orange">橙色</string> <string name="color_orange">橙色</string>
@@ -221,10 +222,9 @@
<string name="yes"></string> <string name="yes"></string>
<string name="no"></string> <string name="no"></string>
<string name="failed_reboot">重启失败</string> <string name="failed_reboot">重启失败</string>
<string name="batch_authorization">批量授权</string> <string name="batch_authorization">授权</string>
<string name="batch_cancel_authorization">批量取消授权</string> <string name="batch_cancel_authorization">撤销</string>
<string name="color_yellow">黄色</string> <string name="color_yellow">黄色</string>
<string name="kpm">内核模块</string>
<string name="kpm_title">内核模块</string> <string name="kpm_title">内核模块</string>
<string name="kpm_empty">暂无已安装的内核模块</string> <string name="kpm_empty">暂无已安装的内核模块</string>
<string name="kpm_version">版本</string> <string name="kpm_version">版本</string>
@@ -284,12 +284,40 @@
<string name="slot_a">A槽位</string> <string name="slot_a">A槽位</string>
<string name="slot_b">B槽位</string> <string name="slot_b">B槽位</string>
<string name="selected_slot">已选择槽位: %1$s</string> <string name="selected_slot">已选择槽位: %1$s</string>
<!-- 错误信息 -->
<string name="horizon_copy_failed">复制失败</string>
<string name="horizon_unknown_error">未知错误</string>
<string name="horizon_getting_original_slot">获取原有槽位</string> <string name="horizon_getting_original_slot">获取原有槽位</string>
<string name="horizon_setting_target_slot">设置指定槽位</string> <string name="horizon_setting_target_slot">设置指定槽位</string>
<string name="horizon_restoring_original_slot">恢复默认槽位</string> <string name="horizon_restoring_original_slot">恢复默认槽位</string>
<string name="current_slot">当前槽位:%1$s </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> <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> </resources>

View File

@@ -118,7 +118,7 @@
<string name="enable_web_debugging">Enable WebView debugging</string> <string name="enable_web_debugging">Enable WebView debugging</string>
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string> <string name="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="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">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_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
<string name="install_next">Next</string> <string name="install_next">Next</string>
@@ -194,6 +194,8 @@
<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_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">Hide SuSFS status</string>
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string> <string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
<string name="hide_link_card">Hide Link Card Status</string>
<string name="hide_link_card_summary">Hide link card information on the home page</string>
<string name="theme_mode">Theme</string> <string name="theme_mode">Theme</string>
<string name="theme_follow_system">Follow system</string> <string name="theme_follow_system">Follow system</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
@@ -202,8 +204,7 @@
<string name="dynamic_color_title">Dynamic colours</string> <string name="dynamic_color_title">Dynamic colours</string>
<string name="dynamic_color_summary">Dynamic colours using system themes</string> <string name="dynamic_color_summary">Dynamic colours using system themes</string>
<string name="choose_theme_color">Choose a theme colour</string> <string name="choose_theme_color">Choose a theme colour</string>
<string name="color_default">White</string> <string name="color_default">Blue</string>
<string name="color_blue">Blue</string>
<string name="color_green">Green</string> <string name="color_green">Green</string>
<string name="color_purple">Purple</string> <string name="color_purple">Purple</string>
<string name="color_orange">Orange</string> <string name="color_orange">Orange</string>
@@ -221,12 +222,11 @@
<string name="yes">Yes</string> <string name="yes">Yes</string>
<string name="no">No</string> <string name="no">No</string>
<string name="failed_reboot">Reboot Failed</string> <string name="failed_reboot">Reboot Failed</string>
<string name="batch_authorization">Bulk license</string> <string name="batch_authorization">empower</string>
<string name="batch_cancel_authorization">Batch cancel authorization</string> <string name="batch_cancel_authorization">withdraw</string>
<string name="backup">Backup</string> <string name="backup">Backup</string>
<string name="color_yellow">Yellow</string> <string name="color_yellow">Yellow</string>
<string name="kpm">Kernel Module</string> <string name="kpm_title">KPM</string>
<string name="kpm_title">Kernel Module</string>
<string name="kpm_empty">No installed kernel modules at this time</string> <string name="kpm_empty">No installed kernel modules at this time</string>
<string name="kpm_version">Version</string> <string name="kpm_version">Version</string>
<string name="kpm_author">Author</string> <string name="kpm_author">Author</string>
@@ -288,12 +288,40 @@
<string name="slot_a">Slot A</string> <string name="slot_a">Slot A</string>
<string name="slot_b">Slot B</string> <string name="slot_b">Slot B</string>
<string name="selected_slot">Selected slot: %1$s</string> <string name="selected_slot">Selected slot: %1$s</string>
<!-- Error Messages -->
<string name="horizon_copy_failed">Copy failed</string>
<string name="horizon_unknown_error">Unknown error</string>
<string name="horizon_getting_original_slot">Getting the original slot</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_setting_target_slot">Setting the specified slot</string>
<string name="horizon_restoring_original_slot">Restore Default Slot</string> <string name="horizon_restoring_original_slot">Restore Default Slot</string>
<string name="current_slot">Current Slot%1$s </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> <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> </resources>

View File

@@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.9.2" agp = "8.10.0"
kotlin = "2.1.10" kotlin = "2.1.10"
ksp = "2.1.10-1.0.30" ksp = "2.1.10-1.0.30"
compose-bom = "2025.04.01" compose-bom = "2025.04.01"