Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2d8fd7e0 | ||
|
|
2b6d418fe6 | ||
|
|
d8b1126b96 | ||
|
|
2eeddcfa80 | ||
|
|
59e3675a36 | ||
|
|
bc386f080d | ||
|
|
2dc1377154 | ||
|
|
610852e2f2 | ||
|
|
15b19bb8ce | ||
|
|
4a598b1837 | ||
|
|
caee2417d6 | ||
|
|
349ca36d4e | ||
|
|
ec86f5caf2 | ||
|
|
b5a5cdfcd2 | ||
|
|
72d799e065 | ||
|
|
d06f22dcd0 | ||
|
|
cb90630f27 | ||
|
|
59ad9204d0 | ||
|
|
cb97c16f5e | ||
|
|
69b48d5345 | ||
|
|
45ed4708c9 | ||
|
|
f3c77bdb3b | ||
|
|
dc0eb9eec1 | ||
|
|
83dd6443cb | ||
|
|
3d77f2d135 | ||
|
|
1ea219bddc | ||
|
|
39adba62d1 | ||
|
|
3526e84e04 | ||
|
|
bfdb706b60 | ||
|
|
a297e07055 |
3
.github/workflows/gki-kernel.yml
vendored
3
.github/workflows/gki-kernel.yml
vendored
@@ -198,6 +198,9 @@ jobs:
|
||||
- name: Make working directory clean to avoid dirty
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
if [ -e common/BUILD.bazel ]; then
|
||||
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
|
||||
fi
|
||||
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||
git config --global user.email "bot@kernelsu.org"
|
||||
git config --global user.name "KernelSUBot"
|
||||
|
||||
@@ -2,22 +2,19 @@
|
||||
|
||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||
|
||||
|
||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||
|
||||
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
||||
|
||||
>
|
||||
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
>
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
|
||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
||||
|
||||
## How to add
|
||||
|
||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
@@ -31,20 +28,17 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
|
||||
|
||||
1. Use the susfs-dev branch directly without any patching
|
||||
|
||||
|
||||
|
||||
## KPM support
|
||||
|
||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||
|
||||
|
||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
## More links
|
||||
|
||||
Projects compiled based on Sukisu and susfs
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
@@ -53,32 +47,34 @@ Projects compiled based on Sukisu and susfs
|
||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES hook:**
|
||||
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks.
|
||||
- For Loadable Kernel Modules (LKM)
|
||||
- Default hooking method for GKI kernels
|
||||
- Requires `CONFIG_KPROBES=y`.
|
||||
2. **Manual hooks:**
|
||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
||||
- Also used for Loadable Kernel Module (LKM)
|
||||
- Default hook method on GKI kernels.
|
||||
- Need `CONFIG_KPROBES=y`
|
||||
|
||||
2. **Manual hook:**
|
||||
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Default hook method on Non-GKI kernels.
|
||||
- Need `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## Usage
|
||||
|
||||
### GKI
|
||||
1. such as Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
|
||||
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
|
||||
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
|
||||
|
||||
Please follow this guide.
|
||||
|
||||
https://kernelsu.org/guide/installation.html
|
||||
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
|
||||
|
||||
> [!Note]
|
||||
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
|
||||
> - Please search for the processor codename by yourself, usually it is all English without numbers.
|
||||
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
1. Kernel-based `su` and root access management.
|
||||
@@ -88,23 +84,19 @@ Projects compiled based on Sukisu and susfs
|
||||
5. More customization
|
||||
6. Support for KPM kernel modules
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
||||
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
## Sponsorship list
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||
|
||||
|
||||
|
||||
|
||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -7,16 +7,15 @@
|
||||
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||
|
||||
>
|
||||
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||
>
|
||||
>ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
|
||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
||||
|
||||
## 追加方法
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
@@ -26,52 +25,57 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
## 統合された susfs の使い方
|
||||
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
|
||||
## KPM に対応
|
||||
|
||||
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||
|
||||
|
||||
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
## その他のリンク
|
||||
|
||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## フックの方式
|
||||
|
||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||
|
||||
1. **KPROBES フック:**
|
||||
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
|
||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||
- GKI カーネルのデフォルトとなるフック方式
|
||||
- `CONFIG_KPROBES=y` が必要です。
|
||||
- `CONFIG_KPROBES=y` が必要です
|
||||
|
||||
2. **手動でフック:**
|
||||
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
|
||||
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
|
||||
|
||||
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||
|
||||
## 使い方
|
||||
|
||||
### GKI
|
||||
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
|
||||
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします。
|
||||
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
|
||||
|
||||
このガイドに従ってください。
|
||||
|
||||
https://kernelsu.org/ja_JP/guide/installation.html
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||
|
||||
> [!Note]
|
||||
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||
|
||||
|
||||
## 機能
|
||||
|
||||
1. カーネルベースな `su` および root アクセスの管理。
|
||||
@@ -81,23 +85,19 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
||||
5. その他のカスタマイズ
|
||||
6. KPM カーネルモジュールに対応
|
||||
|
||||
|
||||
|
||||
## ライセンス
|
||||
|
||||
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||
|
||||
## スポンサーシップの一覧
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
|
||||
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
|
||||
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||
|
||||
|
||||
|
||||
|
||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||
|
||||
## 貢献者
|
||||
|
||||
@@ -6,23 +6,19 @@
|
||||
|
||||
**实验性! 使用风险自负!**
|
||||
|
||||
|
||||
>
|
||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||
> 但是,我们将会在未来成为一个单独维护的KSU分支
|
||||
>
|
||||
|
||||
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||
|
||||
## 如何添加
|
||||
|
||||
在内核源码的根目录下执行以下命令:
|
||||
|
||||
使用 susfs-dev 分支(已集成susfs,带非GKI设备的支持)
|
||||
|
||||
使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
|
||||
使用 main 分支
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
@@ -33,46 +29,49 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||
|
||||
## 钩子方法
|
||||
|
||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES 钩子:**
|
||||
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
|
||||
2. **手动钩子:**
|
||||
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
||||
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
||||
- 非 GKI 内核的默认挂钩方法
|
||||
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## KPM 支持
|
||||
|
||||
## KPM支持
|
||||
|
||||
- 我们基于KernelPatch去掉了和KSU重复的功能,保留了KPM支持
|
||||
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
|
||||
|
||||
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||
|
||||
开源地址: 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 编译的项目
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
### GKI
|
||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
||||
### 普适的 GKI
|
||||
|
||||
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!Note]
|
||||
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||
|
||||
|
||||
### 一加
|
||||
|
||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||
|
||||
> [!Note]
|
||||
@@ -80,7 +79,6 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||
|
||||
|
||||
## 特点
|
||||
|
||||
1. 基于内核的 `su` 和 root 访问管理
|
||||
@@ -88,8 +86,7 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
||||
4. 恢复对非 GKI 2.0 内核的支持
|
||||
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)。
|
||||
|
||||
## 赞助名单
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||
|
||||
|
||||
|
||||
|
||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||
|
||||
## 贡献
|
||||
@@ -118,4 +113,4 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
||||
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch是APatch实现内核模块的关键部分
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分
|
||||
@@ -1,5 +1,6 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||
|
||||
@@ -24,7 +25,17 @@ apksign {
|
||||
keyPasswordProperty = "KEY_PASSWORD"
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
|
||||
/**signingConfigs {
|
||||
create("Debug") {
|
||||
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||
storePassword = ""
|
||||
keyAlias = ""
|
||||
keyPassword = ""
|
||||
}
|
||||
}**/
|
||||
namespace = "com.sukisu.ultra"
|
||||
|
||||
buildTypes {
|
||||
@@ -33,6 +44,9 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
/**debug {
|
||||
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
|
||||
}**/
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
|
||||
<application
|
||||
|
||||
BIN
manager/app/src/main/assets/5_10-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_10-mkbootfs
Normal file
Binary file not shown.
@@ -91,14 +91,6 @@ object Natives {
|
||||
return version < MINIMAL_SUPPORTED_KERNEL
|
||||
}
|
||||
|
||||
fun isKsuValid(pkgName: String?): Boolean {
|
||||
if (becomeManager(pkgName)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
|
||||
@@ -133,15 +133,13 @@ class HorizonKernelWorker(
|
||||
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||
state.updateProgress(0.7f)
|
||||
|
||||
// 获取原始槽位信息
|
||||
if (slot != null) {
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
if (isAbDevice && slot != null) {
|
||||
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||
state.updateProgress(0.72f)
|
||||
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||
}
|
||||
|
||||
// 设置目标槽位
|
||||
if (!slot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||
state.updateProgress(0.74f)
|
||||
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||
@@ -149,8 +147,7 @@ class HorizonKernelWorker(
|
||||
|
||||
flash()
|
||||
|
||||
// 恢复原始槽位
|
||||
if (!originalSlot.isNullOrEmpty()) {
|
||||
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
@@ -165,8 +162,7 @@ class HorizonKernelWorker(
|
||||
} catch (e: Exception) {
|
||||
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||
|
||||
// 恢复原始槽位
|
||||
if (!originalSlot.isNullOrEmpty()) {
|
||||
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
@@ -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() {
|
||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||
}
|
||||
@@ -196,9 +203,25 @@ class HorizonKernelWorker(
|
||||
}
|
||||
|
||||
private fun patch() {
|
||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
||||
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
|
||||
val toolName = if (version.isNotEmpty()) {
|
||||
val parts = version.split('.')
|
||||
if (parts.size >= 2) {
|
||||
val major = parts[0].toIntOrNull() ?: 0
|
||||
val minor = parts[1].toIntOrNull() ?: 0
|
||||
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
|
||||
}
|
||||
|
||||
private fun flash() {
|
||||
@@ -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")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
@@ -354,7 +377,7 @@ fun HorizonKernelFlashProgress(state: FlashState) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 150.dp)
|
||||
.heightIn(max = 230.dp)
|
||||
.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package com.sukisu.ultra.ui
|
||||
|
||||
import android.database.ContentObserver
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
@@ -29,24 +30,68 @@ import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private inner class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadCustomBackground()
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||
runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
val destroyListeners = mutableListOf<() -> Unit>()
|
||||
destroyListeners.add {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
@@ -58,12 +103,13 @@ class MainActivity : ComponentActivity() {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState,
|
||||
LocalSnackbarHost provides snackBarHostState
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
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
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
val cardElevation = CardConfig.cardElevation
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
|
||||
NavigationBar(
|
||||
tonalElevation = cardElevation, // 动态设置阴影
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
||||
)
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
@@ -110,29 +180,33 @@ private fun BottomBar(navController: NavHostController) {
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
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(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -141,26 +215,33 @@ private fun BottomBar(navController: NavHostController) {
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
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(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ fun ImageEditorDialog(
|
||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
|
||||
@Composable
|
||||
fun KsuIsValid(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
|
||||
if (ksuVersion != null) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ fun SearchAppBar(
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
if (onSearch) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SdStorage
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/**
|
||||
* 槽位选择对话框组件
|
||||
* 用于HorizonKernel刷写时选择目标槽位
|
||||
* 用于Kernel刷写时选择目标槽位
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -43,38 +47,26 @@ fun SlotSelectionDialog(
|
||||
}
|
||||
|
||||
if (show) {
|
||||
val backgroundColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast.copy(alpha = 1.0f)
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 1.0f)
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
shape = MaterialTheme.shapes.medium.copy(
|
||||
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())
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
if (errorMessage != null) {
|
||||
Text(
|
||||
text = "Error: $errorMessage",
|
||||
@@ -105,88 +97,134 @@ fun SlotSelectionDialog(
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Horizontal arrangement for slot options with highlighted current slot
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
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(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
val slotOptions = listOf(
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_a),
|
||||
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
),
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_b),
|
||||
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
)
|
||||
)
|
||||
|
||||
slotOptions.forEachIndexed { index, option ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
onSlotSelected(
|
||||
when (index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (true) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前槽位信息
|
||||
// Data class for list options
|
||||
data class ListOption(
|
||||
val titleText: String,
|
||||
val subtitleText: String?,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
// Utility function to get current slot
|
||||
private fun getCurrentSlot(context: Context): String? {
|
||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||
if (it.startsWith("_")) it.substring(1) else it
|
||||
|
||||
@@ -1,32 +1,101 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
// 颜色动画
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
animationSpec = tween(300),
|
||||
label = "iconTint"
|
||||
)
|
||||
|
||||
// 开关颜色
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
leadingContent = { Icon(icon, contentDescription = null) },
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { Text(summary) },
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
colors = switchColors
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
}
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -25,16 +31,21 @@ import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -44,7 +55,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -63,13 +77,13 @@ import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.forceStopApp
|
||||
import com.sukisu.ultra.ui.util.getSepolicy
|
||||
@@ -78,6 +92,7 @@ import com.sukisu.ultra.ui.util.restartApp
|
||||
import com.sukisu.ultra.ui.util.setSepolicy
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -107,9 +122,18 @@ fun AppProfileScreen(
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
title = appInfo.label,
|
||||
packageName = packageName,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
@@ -181,22 +205,50 @@ private fun AppProfileInner(
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = { Text(appLabel) },
|
||||
supportingContent = { Text(packageName) },
|
||||
leadingContent = appIcon,
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = appLabel,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
|
||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
||||
Crossfade(
|
||||
targetState = isRootGranted,
|
||||
label = "RootAccess"
|
||||
) { current ->
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||
) {
|
||||
@@ -211,42 +263,91 @@ private fun AppProfileInner(
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
if (currentMode == Mode.Template) {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
} else if (mode == Mode.Custom) {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode != Mode.Default,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||
when (currentMode) {
|
||||
Mode.Template -> {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
Mode.Custom -> {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
val modifyEnabled = currentMode == Mode.Custom
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = modifyEnabled,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode == Mode.Custom,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = mode == Mode.Custom,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,20 +365,51 @@ private enum class Mode(@StringRes private val res: Int) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
packageName: String,
|
||||
onBack: () -> Unit,
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
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 = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
onClick = onBack,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = Modifier.shadow(
|
||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||
4.dp else 0.dp,
|
||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -287,40 +419,88 @@ private fun ProfileBox(
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
||||
supportingContent = { Text(mode.text) },
|
||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
label = { Text(stringResource(R.string.profile_default)) },
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
)
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = mode.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = Dp.Hairline,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_default),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_template),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_custom),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
}
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
label = { Text(stringResource(R.string.profile_custom)) },
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
@@ -329,13 +509,14 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
@@ -349,45 +530,64 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.launch_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
},
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.force_stop_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
},
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.restart_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
Surface {
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Android,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,42 +15,86 @@ import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||
import com.sukisu.ultra.flash.HorizonKernelState
|
||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||
import com.sukisu.ultra.ui.component.DialogHandle
|
||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||
import com.sukisu.ultra.flash.HorizonKernelState
|
||||
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.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.LkmSelection
|
||||
import com.sukisu.ultra.ui.util.getCurrentKmi
|
||||
import com.sukisu.ultra.ui.util.getSupportedKmis
|
||||
import com.sukisu.ultra.ui.util.isAbDevice
|
||||
import com.sukisu.ultra.ui.util.isInitBoot
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
import com.sukisu.ultra.getKernelVersion
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -69,6 +113,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
val horizonKernelState = remember { HorizonKernelState() }
|
||||
val flashState by horizonKernelState.state.collectAsState()
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
val kernelVersion = getKernelVersion()
|
||||
val isGKI = kernelVersion.isGKI()
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
@@ -123,7 +170,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 槽位选择
|
||||
SlotSelectionDialog(
|
||||
show = showSlotSelectionDialog,
|
||||
show = showSlotSelectionDialog && isAbDevice,
|
||||
onDismiss = { showSlotSelectionDialog = false },
|
||||
onSlotSelected = { slot ->
|
||||
showSlotSelectionDialog = false
|
||||
@@ -133,7 +180,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
summary = summary
|
||||
)
|
||||
installMethod = horizonMethod
|
||||
onInstall()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -149,7 +195,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
@@ -191,12 +237,19 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
SelectInstallMethod(
|
||||
isGKI = isGKI,
|
||||
isAbDevice = isAbDevice,
|
||||
onSelected = { method ->
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) {
|
||||
tempKernelUri = method.uri
|
||||
showSlotSelectionDialog = true
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||
if (isAbDevice) {
|
||||
tempKernelUri = method.uri
|
||||
showSlotSelectionDialog = true
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
@@ -218,32 +271,73 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
)
|
||||
)
|
||||
}
|
||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||
if (method.slot != null) {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_slot,
|
||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||
else stringResource(id = R.string.slot_b)
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||
if (method.slot != null) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_slot,
|
||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||
else stringResource(id = R.string.slot_b)
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null && !flashState.isFlashing,
|
||||
onClick = onClickNext
|
||||
onClick = onClickNext,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +399,11 @@ sealed class InstallMethod {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
private fun SelectInstallMethod(
|
||||
isGKI: Boolean = false,
|
||||
isAbDevice: Boolean = false,
|
||||
onSelected: (InstallMethod) -> Unit = {}
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||
@@ -335,8 +433,16 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = when (currentSelectingMethod) {
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = horizonKernelSummary)
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||
uri,
|
||||
summary = selectFileTip
|
||||
)
|
||||
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||
uri,
|
||||
summary = horizonKernelSummary
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
option?.let {
|
||||
@@ -364,55 +470,237 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
||||
putExtra(
|
||||
Intent.EXTRA_MIME_TYPES,
|
||||
arrayOf("application/octet-stream", "application/zip")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
radioOptions.forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
var LKMExpanded by remember { mutableStateOf(false) }
|
||||
var GKIExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
// LKM 安装/修补
|
||||
if (isGKI) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = { onClick(option) },
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.AutoFixHigh,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.Lkm_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
LKMExpanded = !LKMExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = LKMExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.take(3).forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// anykernel3 刷写
|
||||
if (rootAvailable) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.FileUpload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.GKI_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
GKIExpanded = !GKIExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = GKIExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,77 +715,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val listOptions = supportedKmi.map { value ->
|
||||
val options = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value,
|
||||
subtitleText = null,
|
||||
icon = null
|
||||
titleText = value
|
||||
)
|
||||
}
|
||||
|
||||
var selection: String? = null
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
onSelected(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.select_kmi))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selection = supportedKmi[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != null) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
)
|
||||
}), header = Header.Default(
|
||||
title = stringResource(R.string.select_kmi),
|
||||
), selection = ListSelection.Single(
|
||||
showRadioButtons = true,
|
||||
options = options,
|
||||
) { _, option ->
|
||||
selection = option.titleText
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,23 +752,26 @@ private fun TopBar(
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.install)) },
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.install),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onLkmUpload) {
|
||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
@@ -10,11 +9,13 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -36,6 +37,8 @@ import java.io.BufferedReader
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.*
|
||||
import android.app.Activity
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
/**
|
||||
* KPM 管理界面
|
||||
@@ -53,11 +56,6 @@ fun KpmScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||
val moduleFileName = module.id
|
||||
@@ -103,77 +101,123 @@ fun KpmScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
},
|
||||
title = { Text(kpmInstallMode) },
|
||||
text = { moduleName?.let { Text(stringResource(R.string.kpm_install_mode_description, it)) } },
|
||||
confirmButton = {
|
||||
title = {
|
||||
Text(
|
||||
text = kpmInstallMode,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(kpmInstallModeLoad)
|
||||
moduleName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(kpmInstallModeEmbed)
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Download,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeLoad)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inventory,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeEmbed)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(cancel)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
) {
|
||||
Text(cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
@@ -236,10 +280,12 @@ fun KpmScreen(
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
||||
IconButton(
|
||||
onClick = { viewModel.fetchModuleList() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh)
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -256,39 +302,70 @@ fun KpmScreen(
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install)
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install),
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.kpm_install)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Row(
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(16.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
IconButton(onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
}) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
},
|
||||
modifier = Modifier.size(24.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,15 +375,30 @@ fun KpmScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
@@ -489,13 +581,34 @@ private fun KpmModuleItem(
|
||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||
AlertDialog(
|
||||
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 = {
|
||||
OutlinedTextField(
|
||||
value = viewModel.inputArgs,
|
||||
onValueChange = { viewModel.updateInputArgs(it) },
|
||||
label = { Text(stringResource(R.string.kpm_args)) },
|
||||
placeholder = { Text(module.args) }
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_args),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = module.args,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -512,23 +625,39 @@ private fun KpmModuleItem(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
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(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
Card(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -538,54 +667,77 @@ private fun KpmModuleItem(
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
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 = 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(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
Button(
|
||||
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(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = null
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = onUninstall
|
||||
Button(
|
||||
onClick = onUninstall,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -105,15 +63,14 @@ import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||
import okhttp3.OkHttpClient
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -239,7 +196,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
||||
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
@@ -275,7 +232,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -284,7 +241,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortActionFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
||||
prefs.edit {
|
||||
@@ -300,23 +266,33 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortEnabledFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
||||
prefs.edit {
|
||||
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
||||
}
|
||||
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
||||
}
|
||||
scope.launch {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_modules)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = stringResource(R.string.backup)
|
||||
contentDescription = stringResource(R.string.backup),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -329,7 +305,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.restore)
|
||||
contentDescription = stringResource(R.string.restore),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -346,11 +322,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
floatingActionButton = {
|
||||
if (!hideInstallButton) {
|
||||
val moduleInstall = stringResource(id = R.string.module_install)
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectZipLauncher.launch(
|
||||
@@ -363,16 +334,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = moduleInstall
|
||||
contentDescription = moduleInstall,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = moduleInstall
|
||||
text = moduleInstall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -389,10 +361,25 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -591,10 +578,25 @@ private fun ModuleList(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Extension,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,8 +679,16 @@ fun ModuleItem(
|
||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -706,6 +716,7 @@ fun ModuleItem(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val moduleVersion = stringResource(id = R.string.module_version)
|
||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||
@@ -720,6 +731,7 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -727,7 +739,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -735,7 +748,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -749,7 +763,15 @@ fun ModuleItem(
|
||||
enabled = !module.update,
|
||||
checked = module.enabled,
|
||||
onCheckedChange = onCheckChanged,
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -764,83 +786,70 @@ fun ModuleItem(
|
||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (module.hasActionScript) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = {
|
||||
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
||||
viewModel.markNeedRefresh()
|
||||
},
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
text = stringResource(R.string.action),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
)
|
||||
}
|
||||
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// text = stringResource(R.string.action),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
//)
|
||||
//}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
if (module.hasWebUi) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = { onClick(module) },
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.open)
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.open)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,7 +857,7 @@ fun ModuleItem(
|
||||
|
||||
if (updateUrl.isNotEmpty()) {
|
||||
Button(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove,
|
||||
onClick = { onUpdate(module) },
|
||||
shape = ButtonDefaults.textShape,
|
||||
@@ -859,30 +868,23 @@ fun ModuleItem(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript || !module.hasWebUi) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.module_update)
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript || !module.hasWebUi) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.module_update)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
onClick = { onUninstallClicked(module) },
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
if (!module.remove) {
|
||||
Icon(
|
||||
@@ -894,18 +896,18 @@ fun ModuleItem(
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp).rotate(180f),
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = null,
|
||||
|
||||
)
|
||||
}
|
||||
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
||||
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,4 +934,3 @@ fun ModuleItemPreview() {
|
||||
)
|
||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,16 @@ import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -24,13 +26,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
@@ -42,7 +42,6 @@ import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplat
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -52,25 +51,21 @@ import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.*
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
// region 界面基础设置
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
|
||||
// endregion
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -114,237 +109,413 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||
}
|
||||
}
|
||||
// region 配置项列表
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
if (ksuIsValid) {
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
||||
headlineContent = { Text(profileTemplate) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
|
||||
if (ksuIsValid) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked
|
||||
) {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
}
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
if (ksuIsValid) {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
// 配置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.configuration),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
KsuIsValid {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Fence,
|
||||
title = profileTemplate,
|
||||
summary = stringResource(id = R.string.settings_profile_template_summary),
|
||||
onClick = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
) { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked,
|
||||
onCheckedChange = {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
KsuIsValid {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
onCheckedChange = { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate
|
||||
// 设置分组卡片 - 应用设置
|
||||
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)
|
||||
) {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
if (Natives.isKsuValid(ksuApp.packageName)) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging
|
||||
) {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
}
|
||||
// 更多设置
|
||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = newButtonTitle
|
||||
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)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(newButtonTitle) },
|
||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.BugReport,
|
||||
stringResource(id = R.string.send_log)
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
||||
modifier = Modifier.clickable {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 更多设置
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Settings,
|
||||
title = stringResource(id = R.string.more_settings),
|
||||
summary = stringResource(id = R.string.more_settings),
|
||||
onClick = {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 工具
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.tools),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.BugReport,
|
||||
title = stringResource(id = R.string.send_log),
|
||||
onClick = {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Save,
|
||||
text = stringResource(R.string.save_log),
|
||||
onClick = {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
}
|
||||
)
|
||||
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Share,
|
||||
text = stringResource(R.string.send_log),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
|
||||
showBottomsheet = false
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.save_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val about = stringResource(id = R.string.about)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.ContactPage,
|
||||
about
|
||||
// 设置分组卡片 - 关于
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Info,
|
||||
title = stringResource(R.string.about),
|
||||
onClick = {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(about) },
|
||||
modifier = Modifier.clickable {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,16 +552,11 @@ fun UninstallItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
uninstall
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(uninstall) },
|
||||
modifier = Modifier.clickable {
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Delete,
|
||||
title = stringResource(id = R.string.settings_uninstall),
|
||||
onClick = {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
@@ -436,7 +602,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
@@ -444,29 +610,46 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.settings_uninstall))
|
||||
Text(
|
||||
text = stringResource(R.string.settings_uninstall),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -476,7 +659,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
@@ -484,21 +667,27 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
TextButton(
|
||||
onClick = {
|
||||
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),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -508,24 +697,26 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||
cardAlpha
|
||||
} else {
|
||||
if (systemIsDark) 0.8f else 1f
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SettingsPreview() {
|
||||
SettingScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -13,7 +15,10 @@ import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -21,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -34,6 +40,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@@ -80,44 +87,76 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.refresh))
|
||||
}, onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.refresh)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
)
|
||||
}, onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.backup_allowlist))
|
||||
}, onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.restore_allowlist))
|
||||
}, onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (viewModel.showSystemApps)
|
||||
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.restore_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.RestoreFromTrash,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -127,33 +166,77 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
bottomBar = {
|
||||
// 批量操作按钮,直接放在底部栏
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
AnimatedVisibility(
|
||||
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
||||
enter = slideInVertically(initialOffsetY = { it }),
|
||||
exit = slideOutVertically(targetOffsetY = { it })
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
tonalElevation = cardElevation,
|
||||
shadowElevation = cardElevation
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
viewModel.selectedApps = emptySet()
|
||||
viewModel.showBatchActions = false
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Block,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,15 +253,23 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.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 customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||
}
|
||||
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -214,6 +305,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||
}
|
||||
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -249,6 +344,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.other_apps))
|
||||
}
|
||||
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
@@ -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(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -299,7 +430,7 @@ fun GroupHeader(title: String) {
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -316,33 +447,48 @@ private fun AppItem(
|
||||
onLongClick: () -> Unit,
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = 0.dp,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
.then(
|
||||
if (isSelected)
|
||||
Modifier.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
onTap = { onClick() }
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(app.packageName)
|
||||
FlowRow {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT")
|
||||
} else {
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT")
|
||||
}
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.packageInfo)
|
||||
@@ -350,43 +496,93 @@ private fun AppItem(
|
||||
.build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
.padding(end = 16.dp)
|
||||
.size(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) {
|
||||
Switch(
|
||||
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 {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() }
|
||||
onCheckedChange = { onToggleSelection() },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String) {
|
||||
fun LabelText(label: String, backgroundColor: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp)
|
||||
.padding(top = 2.dp, end = 2.dp)
|
||||
.background(
|
||||
Color.Black,
|
||||
backgroundColor,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 8.sp,
|
||||
fontSize = 10.sp,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -61,7 +62,7 @@ import com.ramcosta.composedestinations.result.getOr
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -79,11 +80,6 @@ fun AppProfileTemplateScreen(
|
||||
val viewModel = viewModel<TemplateViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.templateList.isEmpty()) {
|
||||
@@ -98,6 +94,9 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val context = LocalContext.current
|
||||
@@ -109,6 +108,10 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColorUse.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
|
||||
),
|
||||
onSync = {
|
||||
scope.launch { viewModel.fetchTemplates(true) }
|
||||
},
|
||||
@@ -155,7 +158,6 @@ fun AppProfileTemplateScreen(
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, null) },
|
||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
@@ -205,17 +207,17 @@ private fun TemplateItem(
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context)
|
||||
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
if (template.local) {
|
||||
LabelText(label = "local")
|
||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
} else {
|
||||
LabelText(label = "remote")
|
||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -226,12 +228,20 @@ private fun TopBar(
|
||||
onSync: () -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {},
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.settings_profile_template))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
|
||||
@@ -13,76 +13,95 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val defaultElevation: Dp = 0.dp
|
||||
val settingElevation: Dp = 4.dp
|
||||
val customBackgroundElevation: Dp = 0.dp
|
||||
|
||||
var cardAlpha by mutableStateOf(0.45f)
|
||||
var cardElevation by mutableStateOf(defaultElevation)
|
||||
var cardAlpha by mutableStateOf(1f)
|
||||
var cardElevation by mutableStateOf(settingElevation)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* 保存卡片配置到SharedPreferences
|
||||
*/
|
||||
fun save(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putBoolean("custom_background_enabled", cardElevation == 0.dp)
|
||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载卡片配置
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新阴影启用状态
|
||||
*/
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (enabled) defaultElevation else 0.dp
|
||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||
customBackgroundElevation
|
||||
} else if (enabled) {
|
||||
settingElevation
|
||||
} else {
|
||||
customBackgroundElevation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深色模式默认值
|
||||
*/
|
||||
fun setDarkModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 0.35f
|
||||
cardElevation = 0.dp
|
||||
cardAlpha = 1f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取卡片颜色配置
|
||||
*/
|
||||
@Composable
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||
contentColor = when {
|
||||
CardConfig.isUserLightModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.White
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
|
||||
Color.Black
|
||||
}
|
||||
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
|
||||
Color.White
|
||||
}
|
||||
else -> {
|
||||
Color.White
|
||||
}
|
||||
}
|
||||
contentColor = determineContentColor(originalColor)
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -17,153 +17,256 @@ sealed class ThemeColors {
|
||||
abstract val OnTertiaryContainer: 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() {
|
||||
override val Primary = Color(0xFFFFFFFF)
|
||||
override val Secondary = Color(0xFFF5F5F5)
|
||||
override val Tertiary = Color(0xFFE0E0E0)
|
||||
override val OnPrimary = Color(0xFF616161)
|
||||
override val OnSecondary = Color(0xFF616161)
|
||||
override val OnTertiary = Color(0xFF616161)
|
||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
||||
override val OnPrimaryContainer = Color(0xFF000000)
|
||||
override val OnSecondaryContainer = Color(0xFF000000)
|
||||
override val OnTertiaryContainer = Color(0xFF000000)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
}
|
||||
|
||||
// Blue Theme
|
||||
object Blue : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF1E88E5)
|
||||
override val Secondary = Color(0xFF64B5F6)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFCBE6FC)
|
||||
override val SecondaryContainer = Color(0xFFBBDEFB)
|
||||
override val TertiaryContainer = Color(0xFF90CAF9)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1A2E)
|
||||
override val OnSecondaryContainer = Color(0xFF0A192D)
|
||||
override val OnTertiaryContainer = Color(0xFF071B3D)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||
override val ButtonContrast = Color(0xFF2196F3)
|
||||
|
||||
override val Surface = Color(0xFFF5F9FF)
|
||||
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||
override val OnSurface = Color(0xFF1A1C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||
|
||||
override val Error = Color(0xFFB00020)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||
override val OnErrorContainer = Color(0xFF410008)
|
||||
|
||||
override val Outline = Color(0xFFBAC3CF)
|
||||
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||
override val Background = Color(0xFFFAFCFF)
|
||||
override val OnBackground = Color(0xFF1A1C1E)
|
||||
}
|
||||
|
||||
// Green Theme
|
||||
// 绿色主题
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF4CAF50)
|
||||
override val Secondary = Color(0xFF43A047)
|
||||
override val Primary = Color(0xFF43A047)
|
||||
override val Secondary = Color(0xFF66BB6A)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
||||
override val TertiaryContainer = Color(0xFF81C784)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
||||
override val ButtonContrast = Color(0xFF32CD32)
|
||||
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||
override val ButtonContrast = Color(0xFF43A047)
|
||||
|
||||
override val Surface = Color(0xFFF6FBF6)
|
||||
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||
override val OnSurface = Color(0xFF191C19)
|
||||
override val OnSurfaceVariant = Color(0xFF414941)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||
override val OnErrorContainer = Color(0xFF4A0808)
|
||||
|
||||
override val Outline = Color(0xFFBDC9BF)
|
||||
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||
override val Background = Color(0xFFFBFDFB)
|
||||
override val OnBackground = Color(0xFF191C19)
|
||||
}
|
||||
|
||||
// Purple Theme
|
||||
// 紫色主题
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFF8E24AA)
|
||||
override val Tertiary = Color(0xFF4A148C)
|
||||
override val Secondary = Color(0xFFBA68C8)
|
||||
override val Tertiary = Color(0xFF6A1B9A)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
||||
override val ButtonContrast = Color(0xFFDA70D6)
|
||||
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||
override val ButtonContrast = Color(0xFF9C27B0)
|
||||
|
||||
override val Surface = Color(0xFFFCF6FF)
|
||||
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||
override val OnSurface = Color(0xFF1D1B1E)
|
||||
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||
|
||||
override val Error = Color(0xFFD50000)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||
override val OnErrorContainer = Color(0xFF480000)
|
||||
|
||||
override val Outline = Color(0xFFC9B9D0)
|
||||
override val OutlineVariant = Color(0xFFE8DAED)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1D1B1E)
|
||||
}
|
||||
|
||||
// Orange Theme
|
||||
// 橙色主题
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFB8C00)
|
||||
override val Secondary = Color(0xFFFFB74D)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
||||
override val ButtonContrast = Color(0xFFFF6347)
|
||||
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||
override val TertiaryContainer = Color(0xFFFFD180)
|
||||
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||
override val ButtonContrast = Color(0xFFFF9800)
|
||||
|
||||
override val Surface = Color(0xFFFFF8F3)
|
||||
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||
override val OnSurface = Color(0xFF1F1B16)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||
|
||||
override val Error = Color(0xFFD32F2F)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||
override val OnErrorContainer = Color(0xFF490700)
|
||||
|
||||
override val Outline = Color(0xFFD6C3AD)
|
||||
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1F1B16)
|
||||
}
|
||||
|
||||
// Pink Theme
|
||||
// 粉色主题
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFD81B60)
|
||||
override val Secondary = Color(0xFFF06292)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
||||
override val TertiaryContainer = Color(0xFFE91E63)
|
||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
||||
override val ButtonContrast = Color(0xFFFF1493)
|
||||
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||
override val ButtonContrast = Color(0xFFE91E63)
|
||||
|
||||
override val Surface = Color(0xFFFFF7F9)
|
||||
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||
override val OnSurface = Color(0xFF201A1C)
|
||||
override val OnSurfaceVariant = Color(0xFF534347)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD6BABF)
|
||||
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF201A1C)
|
||||
}
|
||||
|
||||
// Gray Theme
|
||||
// 灰色主题
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF9E9E9E)
|
||||
override val Secondary = Color(0xFF757575)
|
||||
override val Tertiary = Color(0xFF616161)
|
||||
override val Primary = Color(0xFF607D8B)
|
||||
override val Secondary = Color(0xFF90A4AE)
|
||||
override val Tertiary = Color(0xFF455A64)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
||||
override val OnSecondaryContainer = Color(0xFF171717)
|
||||
override val OnTertiaryContainer = Color(0xFF141414)
|
||||
override val ButtonContrast = Color(0xFF696969)
|
||||
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||
override val ButtonContrast = Color(0xFF607D8B)
|
||||
|
||||
override val Surface = Color(0xFFF6F9FB)
|
||||
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||
override val OnSurface = Color(0xFF191C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFBDC1C4)
|
||||
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||
override val Background = Color(0xFFFBFCFE)
|
||||
override val OnBackground = Color(0xFF191C1E)
|
||||
}
|
||||
|
||||
// 黄色主题
|
||||
object Yellow : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFD700)
|
||||
override val Secondary = Color(0xFFFFBC52)
|
||||
override val Tertiary = Color(0xFF795548)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val Primary = Color(0xFFFFC107)
|
||||
override val Secondary = Color(0xFFFFD54F)
|
||||
override val Tertiary = Color(0xFFFF8F00)
|
||||
override val OnPrimary = Color(0xFF000000)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
||||
override val ButtonContrast = Color(0xFFFFD700)
|
||||
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||
override val OnTertiaryContainer = Color(0xFF221200)
|
||||
override val ButtonContrast = Color(0xFFFFC107)
|
||||
|
||||
override val Surface = Color(0xFFFFFAF3)
|
||||
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||
override val OnSurface = Color(0xFF1F1C17)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD1C8AF)
|
||||
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||
override val Background = Color(0xFFFFFCF8)
|
||||
override val OnBackground = Color(0xFF1F1C17)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||
"blue" -> Blue
|
||||
"green" -> Green
|
||||
"purple" -> Purple
|
||||
"orange" -> Orange
|
||||
"pink" -> Pink
|
||||
"gray" -> Gray
|
||||
"white" -> Yellow
|
||||
"yellow" -> Yellow
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -14,19 +18,24 @@ import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -36,39 +45,286 @@ import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
|
||||
/**
|
||||
* 主题配置对象,管理应用的主题相关状态
|
||||
*/
|
||||
object ThemeConfig {
|
||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||
var useDynamicColor by mutableStateOf(false)
|
||||
var backgroundImageLoaded by mutableStateOf(false)
|
||||
var needsResetOnThemeChange by mutableStateOf(false)
|
||||
var isThemeChanging by mutableStateOf(false)
|
||||
var preventBackgroundRefresh by mutableStateOf(false)
|
||||
|
||||
private var lastDarkModeState: Boolean? = null
|
||||
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
|
||||
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
|
||||
lastDarkModeState = currentDarkMode
|
||||
return isChanged
|
||||
}
|
||||
|
||||
fun resetBackgroundState() {
|
||||
if (!preventBackgroundRefresh) {
|
||||
backgroundImageLoaded = false
|
||||
}
|
||||
isThemeChanging = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
*/
|
||||
@Composable
|
||||
private fun getDarkColorScheme() = darkColorScheme(
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||
true -> true
|
||||
false -> false
|
||||
null -> isSystemInDarkTheme()
|
||||
},
|
||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
|
||||
// 检测系统主题变化并保存状态
|
||||
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||
LaunchedEffect(systemIsDark, themeChanged) {
|
||||
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||
ThemeConfig.resetBackgroundState()
|
||||
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
}
|
||||
|
||||
CardConfig.apply {
|
||||
load(context)
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||
}
|
||||
save(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
context.loadThemeMode()
|
||||
context.loadThemeColors()
|
||||
context.loadDynamicColorState()
|
||||
CardConfig.load(context)
|
||||
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
|
||||
// 创建颜色方案
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> createDarkColorScheme()
|
||||
else -> createLightColorScheme()
|
||||
}
|
||||
|
||||
// 根据暗色模式和自定义背景调整卡片配置
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||
|
||||
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||
}
|
||||
|
||||
val bgImagePainter = backgroundUri.value?.let {
|
||||
rememberAsyncImagePainter(
|
||||
model = it,
|
||||
onError = {
|
||||
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
|
||||
ThemeConfig.customBackgroundUri = null
|
||||
context.saveCustomBackground(null)
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("ThemeSystem", "背景图加载成功")
|
||||
ThemeConfig.backgroundImageLoaded = true
|
||||
ThemeConfig.isThemeChanging = false
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit { putBoolean("prevent_background_refresh", true) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val transition = updateTransition(
|
||||
targetState = ThemeConfig.backgroundImageLoaded,
|
||||
label = "bgTransition"
|
||||
)
|
||||
val bgAlpha by transition.animateFloat(
|
||||
label = "bgAlpha",
|
||||
transitionSpec = {
|
||||
spring(
|
||||
dampingRatio = 0.8f,
|
||||
stiffness = 300f
|
||||
)
|
||||
}
|
||||
) { loaded -> if (loaded) 1f else 0f }
|
||||
|
||||
DisposableEffect(systemIsDark) {
|
||||
onDispose {
|
||||
if (ThemeConfig.isThemeChanging) {
|
||||
ThemeConfig.isThemeChanging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
)
|
||||
|
||||
// 自定义背景层
|
||||
backgroundUri.value?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
.alpha(bgAlpha)
|
||||
) {
|
||||
// 背景图片
|
||||
bgImagePainter?.let { painter ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = painter,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
.graphicsLayer {
|
||||
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 亮度调节层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||
else Color.White.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
|
||||
// 边缘渐变遮罩
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||
else Color.Black.copy(alpha = 0.2f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 内容层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态深色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicDarkColorScheme(context: Context) =
|
||||
dynamicDarkColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建动态浅色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicLightColorScheme(context: Context) =
|
||||
dynamicLightColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建深色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
onPrimaryContainer = Color.White,
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
onSecondaryContainer = Color.White,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
onTertiaryContainer = Color.White,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
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),
|
||||
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
|
||||
private fun getLightColorScheme() = lightColorScheme(
|
||||
private fun createLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
@@ -88,163 +344,52 @@ private fun getLightColorScheme() = lightColorScheme(
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
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? {
|
||||
try {
|
||||
return try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
|
||||
val fileName = "custom_background.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
|
||||
val backupFile = File(filesDir, "${fileName}.backup")
|
||||
val outputStream = FileOutputStream(backupFile)
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
return Uri.fromFile(file)
|
||||
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
backupFile.renameTo(file)
|
||||
|
||||
Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
||||
return null
|
||||
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// 保存变换后的背景图片到应用内部存储并更新配置
|
||||
/**
|
||||
* 保存并应用自定义背景
|
||||
*/
|
||||
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||
val finalUri = if (transformation != null) {
|
||||
saveTransformedBackground(uri, transformation)
|
||||
@@ -252,36 +397,73 @@ fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTra
|
||||
copyImageToInternalStorage(uri)
|
||||
}
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", finalUri?.toString())
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = finalUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
|
||||
// 保存背景图片到应用内部存储并更新配置
|
||||
/**
|
||||
* 保存自定义背景
|
||||
*/
|
||||
fun Context.saveCustomBackground(uri: Uri?) {
|
||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", newUri?.toString())
|
||||
if (uri == null) {
|
||||
// 如果清除背景,也重置阻止刷新标志
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
} else {
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
|
||||
if (uri != null) {
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载自定义背景
|
||||
*/
|
||||
fun Context.loadCustomBackground() {
|
||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("custom_background", null)
|
||||
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?) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
@@ -294,52 +476,49 @@ fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||
)
|
||||
}
|
||||
ThemeConfig.forceDarkMode = forceDark
|
||||
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题模式
|
||||
*/
|
||||
fun Context.loadThemeMode() {
|
||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_mode", "system")
|
||||
|
||||
ThemeConfig.forceDarkMode = when(mode) {
|
||||
"dark" -> true
|
||||
"light" -> false
|
||||
else -> null
|
||||
}
|
||||
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题颜色
|
||||
*/
|
||||
fun Context.saveThemeColors(themeName: String) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("theme_colors", themeName)
|
||||
}
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题颜色
|
||||
*/
|
||||
fun Context.loadThemeColors() {
|
||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_colors", "default")
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存动态颜色状态
|
||||
*/
|
||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
@@ -348,28 +527,12 @@ fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动态颜色状态
|
||||
*/
|
||||
fun Context.loadDynamicColorState() {
|
||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("use_dynamic_color", true)
|
||||
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
private fun adjustColor(color: Color): Color {
|
||||
val minLuminance = 0.75f
|
||||
val maxLuminance = 1f
|
||||
var luminance = color.luminance()
|
||||
if (luminance < minLuminance) {
|
||||
luminance = minLuminance
|
||||
} else if (luminance > maxLuminance) {
|
||||
luminance = maxLuminance
|
||||
}
|
||||
return color.copy(luminance)
|
||||
}
|
||||
|
||||
private fun mixColors(color1: Color, color2: Color, ratio: Float): Color {
|
||||
val r = (color1.red * ratio + color2.red * (1 - ratio))
|
||||
val g = (color1.green * ratio + color2.green * (1 - ratio))
|
||||
val b = (color1.blue * ratio + color2.blue * (1 - ratio))
|
||||
val a = (color1.alpha * ratio + color2.alpha * (1 - ratio))
|
||||
return Color(r, g, b, a)
|
||||
}
|
||||
@@ -1,33 +1,108 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = androidx.compose.material3.Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
val Typography = Typography(
|
||||
// 大标题
|
||||
displayLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题
|
||||
headlineLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题栏
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
|
||||
// 主体文字
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
|
||||
// 标签
|
||||
labelLarge = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -63,10 +63,6 @@ fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: Backgrou
|
||||
val safeScale = maxOf(0.1f, transformation.scale)
|
||||
matrix.postScale(safeScale, safeScale)
|
||||
|
||||
// 计算中心点
|
||||
val centerX = targetWidth / 2f
|
||||
val centerY = targetHeight / 2f
|
||||
|
||||
// 计算偏移量,确保不会出现负最大值的问题
|
||||
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||
|
||||
@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
|
||||
|
||||
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
||||
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
|
||||
val result = runCmd(shell, cmd)
|
||||
return result.trim().toIntOrNull() ?: -1
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ class SuperUserViewModel : ViewModel() {
|
||||
|
||||
// 批量操作相关状态
|
||||
var showBatchActions by mutableStateOf(false)
|
||||
private set
|
||||
internal set
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
private set
|
||||
internal set
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
<string name="require_kernel_version" formatted="false">現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください!</string>
|
||||
<string name="settings_umount_modules_default">デフォルトでモジュールのマウントを解除する</string>
|
||||
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
|
||||
<string name="settings_susfs_toggle">Kprobe フックを非表示にする</string>
|
||||
<string name="settings_susfs_toggle_summary">KSU によって生成された Kprobe フックを無効化して、代替となる組み込みの非 Kprobe を有効化します。Kprobe をサポートしない 非 GKI カーネルに適用される同等の機能を実装します。</string>
|
||||
<string name="settings_susfs_toggle">kprobe フックを無効化</string>
|
||||
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
|
||||
<string name="profile_selinux_domain">ドメイン</string>
|
||||
<string name="profile_selinux_rules">ルール</string>
|
||||
@@ -117,7 +116,7 @@
|
||||
<string name="enable_web_debugging">WebView デバッグを有効化する</string>
|
||||
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合でのみ有効化してください</string>
|
||||
<string name="direct_install">直接インストール (推奨)</string>
|
||||
<string name="select_file">ファイルを選択</string>
|
||||
<string name="select_file">パッチを適用する必要があるミラーを選択</string>
|
||||
<string name="install_inactive_slot">非アクティブなスロットにインストール (OTA 後)</string>
|
||||
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
|
||||
\nこのオプションは、OTA が完了した後にのみ使用してください。
|
||||
@@ -171,8 +170,8 @@
|
||||
<string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string>
|
||||
<string name="backup_allowlist">許可リストをバックアップ</string>
|
||||
<string name="restore_allowlist">許可リストを復元</string>
|
||||
<string name="settings_custom_background">カスタム背景を設定</string>
|
||||
<string name="settings_custom_background_summary">カスタム背景を設定します</string>
|
||||
<string name="settings_custom_background">カスタムアプリ背景</string>
|
||||
<string name="settings_custom_background_summary">背景にする画像を選択してください</string>
|
||||
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
|
||||
<string name="settings_restore_default">デフォルトに復元</string>
|
||||
<string name="home_android_version">Android のバージョン</string>
|
||||
@@ -195,16 +194,17 @@
|
||||
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします</string>
|
||||
<string name="hide_susfs_status">SuSFS ステータスを非表示にする</string>
|
||||
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
|
||||
<string name="theme_mode">テーマモード</string>
|
||||
<string name="hide_link_card">リンクカードのステータスを隠す</string>
|
||||
<string name="hide_link_card_summary">ホームページのリンクカード情報を隠す</string>
|
||||
<string name="theme_mode">テーマ</string>
|
||||
<string name="theme_follow_system">システムに従う</string>
|
||||
<string name="theme_light">ライトカラー</string>
|
||||
<string name="theme_dark">ダークカラー</string>
|
||||
<string name="theme_light">ライト</string>
|
||||
<string name="theme_dark">ダーク</string>
|
||||
<string name="manual_hook">手動でフック</string>
|
||||
<string name="dynamic_color_title">ダイナミックカラー</string>
|
||||
<string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string>
|
||||
<string name="choose_theme_color">テーマカラーを選択</string>
|
||||
<string name="color_default">ホワイト</string>
|
||||
<string name="color_blue">ブルー</string>
|
||||
<string name="color_default">ブルー</string>
|
||||
<string name="color_green">グリーン</string>
|
||||
<string name="color_purple">パープル</string>
|
||||
<string name="color_orange">オレンジ</string>
|
||||
@@ -213,8 +213,8 @@
|
||||
<string name="color_ivory">アイボリー</string>
|
||||
<string name="flash_option">ブラシの設定</string>
|
||||
<string name="flash_option_tip">フラッシュするファイルを選択</string>
|
||||
<string name="horizon_kernel">AnyKernel3 をフラッシュ</string>
|
||||
<string name="horizon_kernel_summary">AnyKernel3 をフラッシュします</string>
|
||||
<string name="horizon_kernel">AnyKernel3 をインストール</string>
|
||||
<string name="horizon_kernel_summary">AnyKernel3 カーネルファイルをフラッシュします</string>
|
||||
<string name="root_required">root 権限が必要です</string>
|
||||
<string name="copy_failed">ファイルのコピーに失敗しました</string>
|
||||
<string name="reboot_complete_title">スクラブが完了しました</string>
|
||||
@@ -222,48 +222,107 @@
|
||||
<string name="yes">はい</string>
|
||||
<string name="no">いいえ</string>
|
||||
<string name="failed_reboot">再起動に失敗しました</string>
|
||||
<string name="batch_authorization">bulk ライセンス</string>
|
||||
<string name="batch_authorization">Bulk ライセンス</string>
|
||||
<string name="batch_cancel_authorization">認証を一括でキャンセル</string>
|
||||
<string name="backup">バックアップ</string>
|
||||
<string name="color_yellow">イエロー</string>
|
||||
<string name="kpm">KPM モジュール</string>
|
||||
<string name="kpm">カーネルモジュール</string>
|
||||
<string name="kpm_title">カーネルモジュール</string>
|
||||
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
|
||||
<string name="kpm_version">リリース</string>
|
||||
<string name="kpm_version">バージョン</string>
|
||||
<string name="kpm_author">作者</string>
|
||||
<string name="kpm_uninstall">アンインストール</string>
|
||||
<string name="kpm_uninstall_success">アンインストールに失敗しました</string>
|
||||
<string name="kpm_uninstall_failed">アンインストールに失敗しました</string>
|
||||
<string name="kpm_install">インストールを選択</string>
|
||||
<string name="kpm_install">インストール</string>
|
||||
<string name="kpm_install_success">KPM モジュールの読み込みに成功しました</string>
|
||||
<string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string>
|
||||
<string name="kpm_args">KPM パラメーター</string>
|
||||
<string name="kpm_control">完全</string>
|
||||
<string name="kpm_args">パラメータ</string>
|
||||
<string name="kpm_control">実行</string>
|
||||
<string name="home_kpm_version">KPM のバージョン</string>
|
||||
<string name="close_notice">閉じる</string>
|
||||
<string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string>
|
||||
<string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string>
|
||||
<string name="kpm_control_success">成功</string>
|
||||
<string name="kpm_control_failed">失敗</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献に繋がるでしょう!</string>
|
||||
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献は引き続き感謝しています!</string>
|
||||
<string name="not_supported">非対応</string>
|
||||
<string name="supported">対応</string>
|
||||
<string name="home_kpm_module">KPM モジュール数: %d </string>
|
||||
<string name="home_kpm_module">"KPM モジュールの数: %d "</string>
|
||||
<string name="kpm_invalid_file">無効な KPM ファイル</string>
|
||||
<string name="kernel_patched">カーネルはパッチされていません</string>
|
||||
<string name="kernel_not_enabled">カーネルは未設定です</string>
|
||||
<string name="custom_settings">カスタム設定</string>
|
||||
<string name="kpm_install_mode">インストール</string>
|
||||
<string name="kpm_install_mode">KPM をインストール</string>
|
||||
<string name="kpm_install_mode_load">読み込む</string>
|
||||
<string name="kpm_install_mode_embed">埋め込む</string>
|
||||
<string name="kpm_install_mode_description">モジュール:%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="snackbar_failed_to_check_module_file">モジュールファイルが存在するか確認できません</string>
|
||||
<string name="confirm_uninstall_title">アンインストールを確認</string>
|
||||
<string name="confirm_uninstall_confirm">アンインストール中</string>
|
||||
<string name="confirm_uninstall_dismiss">中止</string>
|
||||
<string name="confirm_uninstall_confirm">アンインストール</string>
|
||||
<string name="confirm_uninstall_dismiss">キャンセル</string>
|
||||
<string name="theme_color">テーマカラー</string>
|
||||
<string name="invalid_file_type">ファイルの種類が正しくありません。.kpm ファイルを選択してください。</string>
|
||||
<string name="invalid_file_type">ファイルの種類が間違っています!.kpm ファイルを選択してください。</string>
|
||||
<string name="confirm_uninstall_title_with_filename">アンインストール</string>
|
||||
<string name="confirm_uninstall_content">次の KPM モジュールがアンインストールされます:\n%s</string>
|
||||
<string name="confirm_uninstall_content">次の KPM がアンインストールされます: %s</string>
|
||||
<string name="settings_susfs_toggle_summary">KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。</string>
|
||||
<string name="image_editor_title">背景画像を調整</string>
|
||||
<string name="image_editor_hint">2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します</string>
|
||||
<string name="background_image_error">イメージを読み込めません</string>
|
||||
<string name="reprovision">再プロビジョニング</string>
|
||||
<!-- Kernel Flash Progress Related -->
|
||||
<string name="horizon_flash_title">カーネルをフラッシュ</string>
|
||||
<string name="horizon_logs_label">ログ:</string>
|
||||
<string name="horizon_flash_complete">フラッシュが完了しました</string>
|
||||
<!-- Flash Status Related -->
|
||||
<string name="horizon_preparing">準備中…</string>
|
||||
<string name="horizon_cleaning_files">ファイルを削除中…</string>
|
||||
<string name="horizon_copying_files">ファイルをコピー中…</string>
|
||||
<string name="horizon_extracting_tool">フラッシュツールを展開中…</string>
|
||||
<string name="horizon_patching_script">フラッシュスクリプトをパッチ中…</string>
|
||||
<string name="horizon_flashing">カーネルをフラッシュ中…</string>
|
||||
<string name="horizon_flash_complete_status">フラッシュが完了しました</string>
|
||||
<!-- Slot selection related strings -->
|
||||
<string name="select_slot_title">フラッシュ先のスロットを選択</string>
|
||||
<string name="select_slot_description">フラッシュする boot のターゲットスロットを選択</string>
|
||||
<string name="slot_a">スロット A</string>
|
||||
<string name="slot_b">スロット B</string>
|
||||
<string name="selected_slot">選択したスロット: %1$s</string>
|
||||
<string name="horizon_getting_original_slot">オリジナルのスロットを取得</string>
|
||||
<string name="horizon_setting_target_slot">指定するスロットを設定</string>
|
||||
<string name="horizon_restoring_original_slot">デフォルトのスロットに復元</string>
|
||||
<string name="current_slot">現在のスロット: %1$s </string>
|
||||
<!-- Error Messages -->
|
||||
<string name="horizon_copy_failed">コピーに失敗しました</string>
|
||||
<string name="horizon_unknown_error">不明なエラー</string>
|
||||
<string name="flash_failed_message">フラッシュに失敗しました</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM の修復またはインストール</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI のインストール</string>
|
||||
<string name="kernel_version_log">カーネルのバージョン: %1$s</string>
|
||||
<string name="tool_version_log">パッチ適用ツールの使用: %1$s</string>
|
||||
<string name="configuration">設定</string>
|
||||
<string name="app_settings">アプリの設定</string>
|
||||
<string name="tools">ツール</string>
|
||||
<string name="currently_selected">現在</string>
|
||||
<!-- String resources used in SuperUser -->
|
||||
<string name="clear">削除</string>
|
||||
<string name="apps_with_root">root アプリの権限</string>
|
||||
<string name="apps_with_custom_profile">カスタマイズされたアプリ構成</string>
|
||||
<string name="other_apps">その他のアプリ</string>
|
||||
<string name="no_apps_found">アプリがありません</string>
|
||||
<string name="selinux_enabled_toast">SELinux 有効</string>
|
||||
<string name="selinux_disabled_toast">SELinux 無効</string>
|
||||
<string name="selinux_change_failed">SELinux ステータスの変更に失敗しました</string>
|
||||
<string name="advanced_settings">高度な設定</string>
|
||||
<string name="appearance_settings">ツールバーをカスタマイズ</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="expand">最高の状態</string>
|
||||
<string name="collapse">設置</string>
|
||||
<string name="susfs_enabled">SuSFS 有効</string>
|
||||
<string name="susfs_disabled">SuSFS 無効</string>
|
||||
<string name="background_set_success">背景の設定が成功しました</string>
|
||||
<string name="background_removed">カスタム背景を削除しました</string>
|
||||
<string name="root_require_for_install">root 権限が必要</string>
|
||||
</resources>
|
||||
|
||||
@@ -201,8 +201,7 @@
|
||||
<string name="dynamic_color_title">Màu sắc động</string>
|
||||
<string name="dynamic_color_summary">Màu sắc động sử dụng chủ đề hệ thống</string>
|
||||
<string name="choose_theme_color">Chọn màu chủ đề</string>
|
||||
<string name="color_default">Trắng</string>
|
||||
<string name="color_blue">Xanh dương</string>
|
||||
<string name="color_default">Xanh dương</string>
|
||||
<string name="color_green">Xanh lá</string>
|
||||
<string name="color_purple">Tím</string>
|
||||
<string name="color_orange">Cam</string>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<string name="enable_web_debugging">启用 WebView 调试</string>
|
||||
<string name="enable_web_debugging_summary">可用于调试 WebUI 。请仅在需要时启用。</string>
|
||||
<string name="direct_install">直接安装(推荐)</string>
|
||||
<string name="select_file">选择一个文件</string>
|
||||
<string name="select_file">选择一个需要修补的镜像</string>
|
||||
<string name="install_inactive_slot">安装到未使用的槽位(OTA 后)</string>
|
||||
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认?</string>
|
||||
<string name="install_next">下一步</string>
|
||||
@@ -172,7 +172,7 @@
|
||||
<string name="restore_allowlist">还原应用列表</string>
|
||||
<string name="settings_custom_background">自定义背景</string>
|
||||
<string name="settings_custom_background_summary">选择一张图片作为应用背景</string>
|
||||
<string name="settings_card_alpha">卡片透明度</string>
|
||||
<string name="settings_card_alpha">卡片不透明度</string>
|
||||
<string name="settings_restore_default">恢复默认</string>
|
||||
<string name="home_android_version">Android 版本</string>
|
||||
<string name="home_device_model">设备</string>
|
||||
@@ -194,6 +194,8 @@
|
||||
<string name="hide_other_info_summary">隐藏主页上的超级用户数、模块数和 KPM 模块数信息</string>
|
||||
<string name="hide_susfs_status">隐藏 SuSFS 状态信息</string>
|
||||
<string name="hide_susfs_status_summary">隐藏主页上的 SuSFS 状态信息</string>
|
||||
<string name="hide_link_card">隐藏链接卡片</string>
|
||||
<string name="hide_link_card_summary">隐藏主页上的链接卡片信息</string>
|
||||
<string name="theme_mode">主题模式</string>
|
||||
<string name="theme_follow_system">跟随系统</string>
|
||||
<string name="theme_light">浅色</string>
|
||||
@@ -202,8 +204,7 @@
|
||||
<string name="dynamic_color_title">动态颜色</string>
|
||||
<string name="dynamic_color_summary">使用系统主题的动态颜色</string>
|
||||
<string name="choose_theme_color">选择主题色</string>
|
||||
<string name="color_default">白色</string>
|
||||
<string name="color_blue">蓝色</string>
|
||||
<string name="color_default">蓝色</string>
|
||||
<string name="color_green">绿色</string>
|
||||
<string name="color_purple">紫色</string>
|
||||
<string name="color_orange">橙色</string>
|
||||
@@ -221,10 +222,9 @@
|
||||
<string name="yes">是</string>
|
||||
<string name="no">否</string>
|
||||
<string name="failed_reboot">重启失败</string>
|
||||
<string name="batch_authorization">批量授权</string>
|
||||
<string name="batch_cancel_authorization">批量取消授权</string>
|
||||
<string name="batch_authorization">授权</string>
|
||||
<string name="batch_cancel_authorization">撤销</string>
|
||||
<string name="color_yellow">黄色</string>
|
||||
<string name="kpm">内核模块</string>
|
||||
<string name="kpm_title">内核模块</string>
|
||||
<string name="kpm_empty">暂无已安装的内核模块</string>
|
||||
<string name="kpm_version">版本</string>
|
||||
@@ -284,12 +284,40 @@
|
||||
<string name="slot_a">A槽位</string>
|
||||
<string name="slot_b">B槽位</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_setting_target_slot">设置指定槽位</string>
|
||||
<string name="horizon_restoring_original_slot">恢复默认槽位</string>
|
||||
<string name="current_slot">当前槽位:%1$s </string>
|
||||
<!-- 错误信息 -->
|
||||
<string name="horizon_copy_failed">复制失败</string>
|
||||
<string name="horizon_unknown_error">未知错误</string>
|
||||
<string name="flash_failed_message">刷写失败</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM修补/安装</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI安装</string>
|
||||
<string name="kernel_version_log">内核版本:%1$s</string>
|
||||
<string name="tool_version_log">使用修补工具:%1$s</string>
|
||||
<string name="configuration">配置</string>
|
||||
<string name="app_settings">应用设置</string>
|
||||
<string name="tools">工具</string>
|
||||
<string name="currently_selected">当前</string>
|
||||
<!-- SuperUser 里用到的字符串资源 -->
|
||||
<string name="clear">清除</string>
|
||||
<string name="apps_with_root">Root 权限应用</string>
|
||||
<string name="apps_with_custom_profile">自定义配置应用</string>
|
||||
<string name="other_apps">其他应用</string>
|
||||
<string name="no_apps_found">未找到应用</string>
|
||||
<string name="selinux_enabled_toast">SELinux 已设置为启用状态</string>
|
||||
<string name="selinux_disabled_toast">SELinux 已设置为禁用状态</string>
|
||||
<string name="selinux_change_failed">SELinux 状态更改失败</string>
|
||||
<string name="advanced_settings">高级设置</string>
|
||||
<string name="appearance_settings">外观设置</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="expand">展开</string>
|
||||
<string name="collapse">收起</string>
|
||||
<string name="susfs_enabled">SuSFS 已启用</string>
|
||||
<string name="susfs_disabled">SuSFS 已禁用</string>
|
||||
<string name="background_set_success">背景设置成功</string>
|
||||
<string name="background_removed">已移除自定义背景</string>
|
||||
<string name="root_require_for_install">需要 root 权限</string>
|
||||
</resources>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<string name="enable_web_debugging">Enable WebView debugging</string>
|
||||
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
|
||||
<string name="direct_install">Direct install (Recommended)</string>
|
||||
<string name="select_file">Select a file</string>
|
||||
<string name="select_file">Select a image that needs to be patched</string>
|
||||
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
|
||||
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
|
||||
<string name="install_next">Next</string>
|
||||
@@ -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_susfs_status">Hide SuSFS status</string>
|
||||
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
|
||||
<string name="hide_link_card">Hide Link Card Status</string>
|
||||
<string name="hide_link_card_summary">Hide link card information on the home page</string>
|
||||
<string name="theme_mode">Theme</string>
|
||||
<string name="theme_follow_system">Follow system</string>
|
||||
<string name="theme_light">Light</string>
|
||||
@@ -202,8 +204,7 @@
|
||||
<string name="dynamic_color_title">Dynamic colours</string>
|
||||
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
|
||||
<string name="choose_theme_color">Choose a theme colour</string>
|
||||
<string name="color_default">White</string>
|
||||
<string name="color_blue">Blue</string>
|
||||
<string name="color_default">Blue</string>
|
||||
<string name="color_green">Green</string>
|
||||
<string name="color_purple">Purple</string>
|
||||
<string name="color_orange">Orange</string>
|
||||
@@ -221,12 +222,11 @@
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="failed_reboot">Reboot Failed</string>
|
||||
<string name="batch_authorization">Bulk license</string>
|
||||
<string name="batch_cancel_authorization">Batch cancel authorization</string>
|
||||
<string name="batch_authorization">empower</string>
|
||||
<string name="batch_cancel_authorization">withdraw</string>
|
||||
<string name="backup">Backup</string>
|
||||
<string name="color_yellow">Yellow</string>
|
||||
<string name="kpm">Kernel Module</string>
|
||||
<string name="kpm_title">Kernel Module</string>
|
||||
<string name="kpm_title">KPM</string>
|
||||
<string name="kpm_empty">No installed kernel modules at this time</string>
|
||||
<string name="kpm_version">Version</string>
|
||||
<string name="kpm_author">Author</string>
|
||||
@@ -288,12 +288,40 @@
|
||||
<string name="slot_a">Slot A</string>
|
||||
<string name="slot_b">Slot B</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_setting_target_slot">Setting the specified slot</string>
|
||||
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
|
||||
<string name="current_slot">Current Slot:%1$s </string>
|
||||
<!-- Error Messages -->
|
||||
<string name="horizon_copy_failed">Copy failed</string>
|
||||
<string name="horizon_unknown_error">Unknown error</string>
|
||||
<string name="flash_failed_message">Flash failed</string>
|
||||
<!-- lkm/gki install -->
|
||||
<string name="Lkm_install_methods">LKM repair/installation</string>
|
||||
<string name="GKI_install_methods">GKI/non-GKI installation</string>
|
||||
<string name="kernel_version_log">Kernel version:%1$s</string>
|
||||
<string name="tool_version_log">Using the patching tool:%1$s</string>
|
||||
<string name="configuration">Configure</string>
|
||||
<string name="app_settings">Application Settings</string>
|
||||
<string name="tools">Tools</string>
|
||||
<string name="currently_selected">Currently</string>
|
||||
<!-- String resources used in SuperUser -->
|
||||
<string name="clear">Removals</string>
|
||||
<string name="apps_with_root">Root Application of permissions</string>
|
||||
<string name="apps_with_custom_profile">Customized Configuration Application</string>
|
||||
<string name="other_apps">Other Applications</string>
|
||||
<string name="no_apps_found">Application not found</string>
|
||||
<string name="selinux_enabled_toast">SELinux Enabled</string>
|
||||
<string name="selinux_disabled_toast">SELinux Disabled</string>
|
||||
<string name="selinux_change_failed">SELinux Status change failed</string>
|
||||
<string name="advanced_settings">Advanced Settings</string>
|
||||
<string name="appearance_settings">Customize the toolbar</string>
|
||||
<string name="back">Comeback</string>
|
||||
<string name="expand">Be in full swing</string>
|
||||
<string name="collapse">put away</string>
|
||||
<string name="susfs_enabled">SuSFS enabled</string>
|
||||
<string name="susfs_disabled">SuSFS disabled</string>
|
||||
<string name="background_set_success">Background set successfully</string>
|
||||
<string name="background_removed">Removed custom backgrounds</string>
|
||||
<string name="root_require_for_install">Requires root privileges</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.9.2"
|
||||
agp = "8.10.0"
|
||||
kotlin = "2.1.10"
|
||||
ksp = "2.1.10-1.0.30"
|
||||
compose-bom = "2025.04.01"
|
||||
|
||||
Reference in New Issue
Block a user