From a14551b3ec679f89855f30eb43a7b1e336e6e675 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:11:33 +0800 Subject: [PATCH] Introducing miuix Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com> --- manager/app/build.gradle.kts | 26 +- manager/app/proguard-rules.pro | 48 - manager/app/src/main/AndroidManifest.xml | 82 +- .../aidl/com/sukisu/zako/IKsuInterface.aidl | 5 +- .../com/sukisu/ultra/KernelSUApplication.kt | 34 +- .../src/main/java/com/sukisu/ultra/Kernels.kt | 22 +- .../src/main/java/com/sukisu/ultra/Natives.kt | 141 +- .../ultra/network/RemoteToolsDownloader.kt | 364 --- .../java/com/sukisu/ultra/ui/KsuService.kt | 94 +- .../java/com/sukisu/ultra/ui/MainActivity.kt | 394 +-- .../ultra/ui/activity/component/BottomBar.kt | 219 -- .../ultra/ui/activity/util/ThemeUtils.kt | 97 - .../ui/activity/util/UltraActivityUtils.kt | 236 -- .../sukisu/ultra/ui/component/AboutCard.kt | 117 - .../sukisu/ultra/ui/component/AppIconImage.kt | 57 + .../sukisu/ultra/ui/component/BottomBar.kt | 69 + .../ultra/ui/component/ChooseKmiDialog.kt | 74 + .../com/sukisu/ultra/ui/component/Dialog.kt | 260 +- .../sukisu/ultra/ui/component/DropdownItem.kt | 46 + .../com/sukisu/ultra/ui/component/EditText.kt | 199 ++ .../ultra/ui/component/FabVisibilityState.kt | 75 - .../ui/component/InstallConfirmationDialog.kt | 441 ---- .../ultra/ui/component/KeyEventBlocker.kt | 2 +- .../{KsuIsValidCheck.kt => KsuValidCheck.kt} | 3 +- .../sukisu/ultra/ui/component/SearchBar.kt | 154 -- .../ultra/ui/component/SendLogDialog.kt | 154 ++ .../sukisu/ultra/ui/component/SettingsItem.kt | 106 - .../ultra/ui/component/SuperDropdown.kt | 437 ++-- .../ultra/ui/component/SuperEditArrow.kt | 132 + .../ultra/ui/component/SuperSearchBar.kt | 408 +++ .../ultra/ui/component/UninstallDialog.kt | 140 ++ .../ui/component/VerticalExpandableFab.kt | 257 -- .../ui/component/filter/BaseFieldFilter.kt | 51 + .../ultra/ui/component/filter/FilterNumber.kt | 82 + .../ui/component/profile/AppProfileConfig.kt | 23 +- .../ui/component/profile/RootProfileConfig.kt | 608 ++--- .../ui/component/profile/TemplateConfig.kt | 137 +- .../ultra/ui/component/rebootListPopup.kt | 79 + .../java/com/sukisu/ultra/ui/screen/About.kt | 207 ++ .../com/sukisu/ultra/ui/screen/AppProfile.kt | 1014 ++++---- .../ultra/ui/screen/BottomBarDestination.kt | 24 - .../ultra/ui/screen/ExecuteModuleAction.kt | 181 +- .../java/com/sukisu/ultra/ui/screen/Flash.kt | 805 ++---- .../java/com/sukisu/ultra/ui/screen/Home.kt | 1211 ++++----- .../com/sukisu/ultra/ui/screen/Install.kt | 1068 ++------ .../java/com/sukisu/ultra/ui/screen/Kpm.kt | 742 ------ .../sukisu/ultra/ui/screen/LogViewerScreen.kt | 941 ------- .../java/com/sukisu/ultra/ui/screen/Module.kt | 2067 +++++++-------- .../com/sukisu/ultra/ui/screen/Settings.kt | 1081 +++----- .../com/sukisu/ultra/ui/screen/SuperUser.kt | 1379 ++++------ .../com/sukisu/ultra/ui/screen/Template.kt | 518 ++-- .../sukisu/ultra/ui/screen/TemplateEditor.kt | 381 +-- .../ultra/ui/screen/UmountManagerScreen.kt | 422 ---- .../com/sukisu/ultra/ui/susfs/SuSFSConfig.kt | 2211 ----------------- .../ui/susfs/component/SuSFSConfigDialogs.kt | 1733 ------------- .../ui/susfs/component/SuSFSConfigTabs.kt | 928 ------- .../ultra/ui/susfs/util/SuSFSManager.kt | 1526 ------------ .../ultra/ui/susfs/util/SuSFSModuleScripts.kt | 555 ----- .../com/sukisu/ultra/ui/theme/CardManage.kt | 192 -- .../java/com/sukisu/ultra/ui/theme/Color.kt | 615 ----- .../java/com/sukisu/ultra/ui/theme/Theme.kt | 593 +---- .../java/com/sukisu/ultra/ui/theme/Type.kt | 108 - .../ui/theme/component/ImageEditorDialog.kt | 411 --- .../ultra/ui/theme/util/BackgroundUtils.kt | 110 - .../ultra/ui/util/CompositionProvider.kt | 8 - .../com/sukisu/ultra/ui/util/Downloader.kt | 256 +- .../sukisu/ultra/ui/util/HanziToPinyin.java | 576 +++++ .../com/sukisu/ultra/ui/util/HanziToPinyin.kt | 522 ---- .../com/sukisu/ultra/ui/util/HyperlinkText.kt | 88 - .../java/com/sukisu/ultra/ui/util/KsuCli.kt | 318 +-- .../sukisu/ultra/ui/util/SELinuxChecker.kt | 41 +- .../com/sukisu/ultra/ui/util/UidGroupUtils.kt | 56 + .../ultra/ui/util/module/LatestVersionInfo.kt | 7 +- .../ultra/ui/util/module/ModuleModify.kt | 457 ---- .../ultra/ui/util/module/ModuleUtils.kt | 139 -- .../util/module/ModuleVerificationManager.kt | 233 -- .../ultra/ui/viewmodel/HomeViewModel.kt | 590 ----- .../sukisu/ultra/ui/viewmodel/KpmViewModel.kt | 160 -- .../ultra/ui/viewmodel/ModuleViewModel.kt | 613 ++--- .../ultra/ui/viewmodel/SuperUserViewModel.kt | 495 ++-- .../ultra/ui/viewmodel/TemplateViewModel.kt | 36 +- .../com/sukisu/ultra/ui/webui/AppIconUtil.kt | 2 +- .../java/com/sukisu/ultra/ui/webui/Insets.kt | 2 +- .../sukisu/ultra/ui/webui/KsuLibSuProvider.kt | 56 - .../com/sukisu/ultra/ui/webui/MimeUtil.java | 88 + .../com/sukisu/ultra/ui/webui/MimeUtil.kt | 77 - .../ultra/ui/webui/SuFilePathHandler.java | 210 ++ .../ultra/ui/webui/SuFilePathHandler.kt | 192 -- .../com/sukisu/ultra/ui/webui/SuService.kt | 14 - .../sukisu/ultra/ui/webui/WebUIActivity.kt | 50 +- .../sukisu/ultra/ui/webui/WebUIXActivity.kt | 116 - .../sukisu/ultra/ui/webui/WebViewInterface.kt | 95 +- .../java/com/sukisu/ultra/utils/AssetsUtil.kt | 26 - .../zakoui/screen/kernelFlash/KernelFlash.kt | 475 ---- .../component/SlotSelectionDialog.kt | 258 -- .../kernelFlash/state/KernelFlashState.kt | 524 ---- .../screen/moreSettings/MoreSettings.kt | 757 ------ .../moreSettings/MoreSettingsHandlers.kt | 459 ---- .../component/MoreSettingsComponents.kt | 201 -- .../component/MoreSettingsDialogs.kt | 620 ----- .../moreSettings/state/MoreSettingsState.kt | 101 - .../screen/moreSettings/util/LocaleHelper.kt | 154 -- .../moreSettings/util/RestartActivityUtils.kt | 27 - .../app/src/main/res/values-ar/strings.xml | 343 +-- .../app/src/main/res/values-az/strings.xml | 374 +-- .../src/main/res/values-bn-rBD/strings.xml | 9 +- .../app/src/main/res/values-bn/strings.xml | 13 +- .../app/src/main/res/values-bs/strings.xml | 410 +-- .../app/src/main/res/values-da/strings.xml | 436 +--- .../app/src/main/res/values-de/strings.xml | 406 +-- .../app/src/main/res/values-es/strings.xml | 330 +-- .../app/src/main/res/values-et/strings.xml | 394 +-- .../app/src/main/res/values-fa/strings.xml | 320 +-- .../app/src/main/res/values-fil/strings.xml | 431 +--- .../app/src/main/res/values-fr/strings.xml | 392 +-- .../app/src/main/res/values-hi/strings.xml | 424 +--- .../app/src/main/res/values-hr/strings.xml | 404 +-- .../app/src/main/res/values-hu/strings.xml | 402 +-- .../app/src/main/res/values-idn/strings.xml | 537 ---- .../app/src/main/res/values-in/strings.xml | 708 +----- .../app/src/main/res/values-it/strings.xml | 302 +-- .../app/src/main/res/values-iw/strings.xml | 14 +- .../app/src/main/res/values-ja/strings.xml | 664 +---- .../app/src/main/res/values-km/strings.xml | 6 + .../app/src/main/res/values-kn/strings.xml | 412 +-- .../app/src/main/res/values-ko/strings.xml | 406 +-- .../app/src/main/res/values-lt/strings.xml | 350 +-- .../app/src/main/res/values-lv/strings.xml | 351 +-- .../app/src/main/res/values-mr/strings.xml | 342 +-- .../app/src/main/res/values-ms/strings.xml | 380 +-- .../app/src/main/res/values-my/strings.xml | 2 + .../app/src/main/res/values-nl/strings.xml | 327 +-- .../app/src/main/res/values-pl/strings.xml | 356 +-- .../src/main/res/values-pt-rBR/strings.xml | 25 +- .../app/src/main/res/values-pt/strings.xml | 359 +-- .../app/src/main/res/values-ro/strings.xml | 318 +-- .../app/src/main/res/values-ru/strings.xml | 735 +----- .../app/src/main/res/values-sl/strings.xml | 354 +-- .../app/src/main/res/values-sr/strings.xml | 4 +- .../app/src/main/res/values-te/strings.xml | 4 +- .../app/src/main/res/values-th/strings.xml | 380 +-- .../app/src/main/res/values-tr/strings.xml | 750 +----- .../app/src/main/res/values-uk/strings.xml | 565 +---- .../app/src/main/res/values-vi/strings.xml | 756 +----- .../src/main/res/values-zh-rCN/strings.xml | 660 +---- .../src/main/res/values-zh-rHK/strings.xml | 676 +---- .../src/main/res/values-zh-rTW/strings.xml | 755 +----- manager/app/src/main/res/values/colors.xml | 2 +- manager/app/src/main/res/values/strings.xml | 918 ++----- manager/build.gradle.kts | 27 +- manager/gradle/libs.versions.toml | 40 +- manager/gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 45633 bytes manager/gradlew | 3 +- manager/gradlew.bat | 1 + manager/sign.example.properties | 2 +- userspace/ksud/src/cli.rs | 2 +- 156 files changed, 10788 insertions(+), 42788 deletions(-) delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/network/RemoteToolsDownloader.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/AppIconImage.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/BottomBar.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/ChooseKmiDialog.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/DropdownItem.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/EditText.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/FabVisibilityState.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt rename manager/app/src/main/java/com/sukisu/ultra/ui/component/{KsuIsValidCheck.kt => KsuValidCheck.kt} (91%) delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SendLogDialog.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperEditArrow.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperSearchBar.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/UninstallDialog.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/BaseFieldFilter.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/FilterNumber.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/rebootListPopup.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/About.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/UidGroupUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuService.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt delete mode 100644 manager/app/src/main/res/values-idn/strings.xml create mode 100644 manager/app/src/main/res/values-km/strings.xml create mode 100644 manager/app/src/main/res/values-my/strings.xml diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index d51afa9c..be818b79 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -10,8 +10,6 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.lsplugin.apksign) id("kotlin-parcelize") - - } val managerVersionCode: Int by rootProject.extra @@ -25,7 +23,6 @@ apksign { keyPasswordProperty = "KEY_PASSWORD" } - android { /**signingConfigs { @@ -117,13 +114,8 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.foundation) - implementation(libs.androidx.documentfile) - implementation(libs.androidx.compose.foundation) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) @@ -145,24 +137,12 @@ dependencies { implementation(libs.kotlinx.coroutines.core) - implementation(libs.me.zhanghai.android.appiconloader.coil) - - implementation(libs.sheet.compose.dialogs.core) - implementation(libs.sheet.compose.dialogs.list) - implementation(libs.sheet.compose.dialogs.input) - implementation(libs.markdown) implementation(libs.androidx.webkit) implementation(libs.lsposed.cxx) - implementation(libs.com.github.topjohnwu.libsu.core) - - implementation(libs.mmrl.platform) - compileOnly(libs.mmrl.hidden.api) - implementation(libs.mmrl.webui) - implementation(libs.mmrl.ui) - - implementation(libs.accompanist.drawablepainter) - + implementation(libs.miuix) + implementation(libs.haze) + implementation(libs.capsule) } \ No newline at end of file diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 18c49c15..e69de29b 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -1,48 +0,0 @@ --verbose --optimizationpasses 5 - --dontwarn org.conscrypt.** --dontwarn kotlinx.serialization.** - -# Please add these rules to your existing keep rules in order to suppress warnings. -# This is generated automatically by the Android Gradle plugin. --dontwarn com.google.auto.service.AutoService --dontwarn com.google.j2objc.annotations.RetainedWith --dontwarn javax.lang.model.SourceVersion --dontwarn javax.lang.model.element.AnnotationMirror --dontwarn javax.lang.model.element.AnnotationValue --dontwarn javax.lang.model.element.Element --dontwarn javax.lang.model.element.ElementKind --dontwarn javax.lang.model.element.ElementVisitor --dontwarn javax.lang.model.element.ExecutableElement --dontwarn javax.lang.model.element.Modifier --dontwarn javax.lang.model.element.Name --dontwarn javax.lang.model.element.PackageElement --dontwarn javax.lang.model.element.TypeElement --dontwarn javax.lang.model.element.TypeParameterElement --dontwarn javax.lang.model.element.VariableElement --dontwarn javax.lang.model.type.ArrayType --dontwarn javax.lang.model.type.DeclaredType --dontwarn javax.lang.model.type.ExecutableType --dontwarn javax.lang.model.type.TypeKind --dontwarn javax.lang.model.type.TypeMirror --dontwarn javax.lang.model.type.TypeVariable --dontwarn javax.lang.model.type.TypeVisitor --dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8 --dontwarn javax.lang.model.util.AbstractTypeVisitor8 --dontwarn javax.lang.model.util.ElementFilter --dontwarn javax.lang.model.util.Elements --dontwarn javax.lang.model.util.SimpleElementVisitor8 --dontwarn javax.lang.model.util.SimpleTypeVisitor7 --dontwarn javax.lang.model.util.SimpleTypeVisitor8 --dontwarn javax.lang.model.util.Types --dontwarn javax.tools.Diagnostic$Kind - - -# MMRL:webui reflection --keep class com.dergoogler.mmrl.webui.interfaces.** { *; } --keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } - --keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } - --keep interface com.sukisu.zako.** { *; } \ No newline at end of file diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index d4f5d53d..025709cc 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -3,98 +3,29 @@ xmlns:tools="http://schemas.android.com/tools"> - - - - - - + android:theme="@style/Theme.KernelSU" + android:windowSoftInputMode="adjustResize"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - getPackages(int start, int maxCount); + ParcelableListSlice getPackages(int flags); } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt index 2f587d13..d63bddcb 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt @@ -2,18 +2,8 @@ package com.sukisu.ultra import android.app.Application import android.system.Os -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import coil.Coil -import coil.ImageLoader -import com.dergoogler.mmrl.platform.Platform -import me.zhanghai.android.appiconloader.coil.AppIconFetcher -import me.zhanghai.android.appiconloader.coil.AppIconKeyer import okhttp3.Cache import okhttp3.OkHttpClient import java.io.File @@ -30,25 +20,6 @@ class KernelSUApplication : Application(), ViewModelStoreOwner { super.onCreate() ksuApp = this - // For faster response when first entering superuser or webui activity - val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java] - CoroutineScope(Dispatchers.Main).launch { - superUserViewModel.fetchAppList() - } - - Platform.setHiddenApiExemptions() - - val context = this - val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size) - Coil.setImageLoader( - ImageLoader.Builder(context) - .components { - add(AppIconKeyer()) - add(AppIconFetcher.Factory(iconSize, false, context)) - } - .build() - ) - val webroot = File(dataDir, "webroot") if (!webroot.exists()) { webroot.mkdir() @@ -62,11 +33,12 @@ class KernelSUApplication : Application(), ViewModelStoreOwner { .addInterceptor { block -> block.proceed( block.request().newBuilder() - .header("User-Agent", "SukiSU/${BuildConfig.VERSION_CODE}") + .header("User-Agent", "KernelSU/${BuildConfig.VERSION_CODE}") .header("Accept-Language", Locale.getDefault().toLanguageTag()).build() ) }.build() } + override val viewModelStore: ViewModelStore get() = appViewModelStore -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt b/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt index 597ac1cd..26219505 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt @@ -8,11 +8,23 @@ import android.system.Os */ data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) { - override fun toString(): String = "$major.$patchLevel.$subLevel" - fun isGKI(): Boolean = when { - major > 5 -> true - major == 5 && patchLevel >= 10 -> true - else -> false + override fun toString(): String { + return "$major.$patchLevel.$subLevel" + } + + fun isGKI(): Boolean { + + // kernel 6.x + if (major > 5) { + return true + } + + // kernel 5.10.x + if (major == 5) { + return patchLevel >= 10 + } + + return false } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt index db891dab..b67c9593 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt @@ -17,51 +17,14 @@ object Natives { // 10977: change groups_count and groups to avoid overflow write // 11071: Fix the issue of failing to set a custom SELinux type. // 12143: breaking: new supercall impl - const val MINIMAL_SUPPORTED_KERNEL = 12143 + const val MINIMAL_SUPPORTED_KERNEL = 22000 - // 12040: Support disable sucompat mode const val KERNEL_SU_DOMAIN = "u:r:su:s0" - const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.8" - - const val MINIMAL_SUPPORTED_KPM = 12800 - - const val MINIMAL_SUPPORTED_DYNAMIC_MANAGER = 13215 - - const val MINIMAL_SUPPORTED_UID_SCANNER = 13347 - - const val MINIMAL_NEW_IOCTL_KERNEL = 13490 - const val ROOT_UID = 0 const val ROOT_GID = 0 - // 获取完整版本号 - external fun getFullVersion(): String - - fun isVersionLessThan(v1Full: String, v2Full: String): Boolean { - fun extractVersionParts(version: String): List { - val match = Regex("""v\d+(\.\d+)*""").find(version) - val simpleVersion = match?.value ?: version - return simpleVersion.trimStart('v').split('.').map { it.toIntOrNull() ?: 0 } - } - - val v1Parts = extractVersionParts(v1Full) - val v2Parts = extractVersionParts(v2Full) - val maxLength = maxOf(v1Parts.size, v2Parts.size) - for (i in 0 until maxLength) { - val num1 = v1Parts.getOrElse(i) { 0 } - val num2 = v2Parts.getOrElse(i) { 0 } - if (num1 != num2) return num1 < num2 - } - return false - } - - fun getSimpleVersionFull(): String = getFullVersion().let { version -> - Regex("""v\d+(\.\d+)*""").find(version)?.value ?: version - } - init { - System.loadLibrary("zakosign") System.loadLibrary("kernelsu") } @@ -119,72 +82,8 @@ object Natives { external fun setEnhancedSecurityEnabled(enabled: Boolean): Boolean /** - * Su Log can be enabled/disabled. - * 0: disabled - * 1: enabled - * negative : error + * Get the user name for the uid. */ - external fun isSuLogEnabled(): Boolean - external fun setSuLogEnabled(enabled: Boolean): Boolean - - external fun isKPMEnabled(): Boolean - external fun getHookType(): String - - /** - * Get SUSFS feature status from kernel - * @return SusfsFeatureStatus object containing all feature states, or null if failed - */ - - /** - * Set dynamic managerature configuration - * @param size APK signature size - * @param hash APK signature hash (64 character hex string) - * @return true if successful, false otherwise - */ - external fun setDynamicManager(size: Int, hash: String): Boolean - - - /** - * Get current dynamic managerature configuration - * @return DynamicManagerConfig object containing current configuration, or null if not set - */ - external fun getDynamicManager(): DynamicManagerConfig? - - /** - * Clear dynamic managerature configuration - * @return true if successful, false otherwise - */ - external fun clearDynamicManager(): Boolean - - /** - * Get active managers list when dynamic manager is enabled - * @return ManagersList object containing active managers, or null if failed or not enabled - */ - external fun getManagersList(): ManagersList? - - // 模块签名验证 - external fun verifyModuleSignature(modulePath: String): Boolean - - /** - * Check if UID scanner is currently enabled - * @return true if UID scanner is enabled, false otherwise - */ - external fun isUidScannerEnabled(): Boolean - - /** - * Enable or disable UID scanner - * @param enabled true to enable, false to disable - * @return true if operation was successful, false otherwise - */ - external fun setUidScannerEnabled(enabled: Boolean): Boolean - - /** - * Clear UID scanner environment (force exit) - * This will forcefully stop all UID scanner operations and clear the environment - * @return true if operation was successful, false otherwise - */ - external fun clearUidScannerEnvironment(): Boolean - external fun getUserName(uid: Int): String? private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" @@ -208,41 +107,9 @@ object Natives { } fun requireNewKernel(): Boolean { - if (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) return true - return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL) + return version != -1 && version < MINIMAL_SUPPORTED_KERNEL } - @Immutable - @Parcelize - @Keep - data class DynamicManagerConfig( - val size: Int = 0, - val hash: String = "" - ) : Parcelable { - - fun isValid(): Boolean { - return size > 0 && hash.length == 64 && hash.all { - it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' - } - } - } - - @Immutable - @Parcelize - @Keep - data class ManagersList( - val count: Int = 0, - val managers: List = emptyList() - ) : Parcelable - - @Immutable - @Parcelize - @Keep - data class ManagerInfo( - val uid: Int = 0, - val signatureIndex: Int = 0 - ) : Parcelable - @Immutable @Parcelize @Keep @@ -278,4 +145,4 @@ object Natives { constructor() : this("") } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/network/RemoteToolsDownloader.kt b/manager/app/src/main/java/com/sukisu/ultra/network/RemoteToolsDownloader.kt deleted file mode 100644 index 880b2ea3..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/network/RemoteToolsDownloader.kt +++ /dev/null @@ -1,364 +0,0 @@ -package com.sukisu.ultra.network - -import android.content.Context -import android.util.Log -import kotlinx.coroutines.* -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.net.HttpURLConnection -import java.net.SocketTimeoutException -import java.net.URL -import java.util.concurrent.TimeUnit - -class RemoteToolsDownloader( - private val context: Context, - private val workDir: String -) { - companion object { - private const val TAG = "RemoteToolsDownloader" - - // 远程下载URL配置 - private const val KPTOOLS_REMOTE_URL = "https://raw.githubusercontent.com/ShirkNeko/SukiSU_patch/refs/heads/main/kpm/kptools" - private const val KPIMG_REMOTE_URL = "https://raw.githubusercontent.com/ShirkNeko/SukiSU_patch/refs/heads/main/kpm/kpimg" - - // 网络超时配置(毫秒) - private const val CONNECTION_TIMEOUT = 15000 // 15秒连接超时 - private const val READ_TIMEOUT = 30000 // 30秒读取超时 - - // 最大重试次数 - private const val MAX_RETRY_COUNT = 3 - - // 文件校验相关 - private const val MIN_FILE_SIZE = 1024 - } - - interface DownloadProgressListener { - fun onProgress(fileName: String, progress: Int, total: Int) - fun onLog(message: String) - fun onError(fileName: String, error: String) - fun onSuccess(fileName: String, isRemote: Boolean) - } - - data class DownloadResult( - val success: Boolean, - val isRemoteSource: Boolean, - val errorMessage: String? = null - ) - - - suspend fun downloadToolsAsync(listener: DownloadProgressListener?): Map = withContext(Dispatchers.IO) { - val results = mutableMapOf() - - listener?.onLog("Starting to prepare KPM tool files...") - - try { - // 确保工作目录存在 - File(workDir).mkdirs() - - // 并行下载两个工具文件 - val kptoolsDeferred = async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) } - val kpimgDeferred = async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) } - - // 等待所有下载完成 - results["kptools"] = kptoolsDeferred.await() - results["kpimg"] = kpimgDeferred.await() - - // 检查kptools执行权限 - val kptoolsFile = File(workDir, "kptools") - if (kptoolsFile.exists()) { - setExecutablePermission(kptoolsFile.absolutePath) - listener?.onLog("Set kptools execution permission") - } - - val successCount = results.values.count { it.success } - val remoteCount = results.values.count { it.success && it.isRemoteSource } - - listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount") - - } catch (e: Exception) { - Log.e(TAG, "Exception occurred while downloading tools", e) - listener?.onLog("Exception occurred during tool download: ${e.message}") - - if (!results.containsKey("kptools")) { - results["kptools"] = downloadSingleTool("kptools", null, listener) - } - if (!results.containsKey("kpimg")) { - results["kpimg"] = downloadSingleTool("kpimg", null, listener) - } - } - - results.toMap() - } - - private suspend fun downloadSingleTool( - fileName: String, - remoteUrl: String?, - listener: DownloadProgressListener? - ): DownloadResult = withContext(Dispatchers.IO) { - - val targetFile = File(workDir, fileName) - - if (remoteUrl == null) { - return@withContext useLocalVersion(fileName, targetFile, listener) - } - - // 尝试从远程下载 - listener?.onLog("Downloading $fileName from remote repository...") - - var lastError = "" - - // 重试机制 - repeat(MAX_RETRY_COUNT) { attempt -> - try { - val result = downloadFromRemote(fileName, remoteUrl, targetFile, listener) - if (result.success) { - listener?.onSuccess(fileName, true) - return@withContext result - } - lastError = result.errorMessage ?: "Unknown error" - - } catch (e: Exception) { - lastError = e.message ?: "Network exception" - Log.w(TAG, "$fileName download attempt ${attempt + 1} failed", e) - - if (attempt < MAX_RETRY_COUNT - 1) { - listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...") - delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L)) - } - } - } - - // 所有重试都失败,回退到本地版本 - listener?.onError(fileName, "Remote download failed: $lastError") - listener?.onLog("$fileName remote download failed, falling back to local version...") - - useLocalVersion(fileName, targetFile, listener) - } - - private suspend fun downloadFromRemote( - fileName: String, - remoteUrl: String, - targetFile: File, - listener: DownloadProgressListener? - ): DownloadResult = withContext(Dispatchers.IO) { - - var connection: HttpURLConnection? = null - - try { - val url = URL(remoteUrl) - connection = url.openConnection() as HttpURLConnection - - // 设置连接参数 - connection.apply { - connectTimeout = CONNECTION_TIMEOUT - readTimeout = READ_TIMEOUT - requestMethod = "GET" - setRequestProperty("User-Agent", "SukiSU-KPM-Downloader/1.0") - setRequestProperty("Accept", "*/*") - setRequestProperty("Connection", "close") - } - - // 建立连接 - connection.connect() - - val responseCode = connection.responseCode - if (responseCode != HttpURLConnection.HTTP_OK) { - return@withContext DownloadResult( - false, - isRemoteSource = false, - errorMessage = "HTTP error code: $responseCode" - ) - } - - val fileLength = connection.contentLength - Log.d(TAG, "$fileName remote file size: $fileLength bytes") - - // 创建临时文件 - val tempFile = File(targetFile.absolutePath + ".tmp") - - // 下载文件 - connection.inputStream.use { input -> - FileOutputStream(tempFile).use { output -> - val buffer = ByteArray(8192) - var totalBytes = 0 - var bytesRead: Int - - while (input.read(buffer).also { bytesRead = it } != -1) { - // 检查协程是否被取消 - ensureActive() - - output.write(buffer, 0, bytesRead) - totalBytes += bytesRead - - // 更新下载进度 - if (fileLength > 0) { - listener?.onProgress(fileName, totalBytes, fileLength) - } - } - - output.flush() - } - } - - // 验证下载的文件 - if (!validateDownloadedFile(tempFile, fileName)) { - tempFile.delete() - return@withContext DownloadResult( - success = false, - isRemoteSource = false, - errorMessage = "File verification failed" - ) - } - - // 移动临时文件到目标位置 - if (targetFile.exists()) { - targetFile.delete() - } - - if (!tempFile.renameTo(targetFile)) { - tempFile.delete() - return@withContext DownloadResult( - false, - isRemoteSource = false, - errorMessage = "Failed to move file" - ) - } - - Log.i(TAG, "$fileName remote download successful, file size: ${targetFile.length()} bytes") - listener?.onLog("$fileName remote download successful") - - DownloadResult(true, isRemoteSource = true) - - } catch (e: SocketTimeoutException) { - Log.w(TAG, "$fileName download timeout", e) - DownloadResult(false, isRemoteSource = false, errorMessage = "Connection timeout") - } catch (e: IOException) { - Log.w(TAG, "$fileName network IO exception", e) - DownloadResult(false, - isRemoteSource = false, - errorMessage = "Network connection exception: ${e.message}" - ) - } catch (e: Exception) { - Log.e(TAG, "$fileName exception occurred during download", e) - DownloadResult(false, - isRemoteSource = false, - errorMessage = "Download exception: ${e.message}" - ) - } finally { - connection?.disconnect() - } - } - - private suspend fun useLocalVersion( - fileName: String, - targetFile: File, - listener: DownloadProgressListener? - ): DownloadResult = withContext(Dispatchers.IO) { - - try { - com.sukisu.ultra.utils.AssetsUtil.exportFiles(context, fileName, targetFile.absolutePath) - - if (!targetFile.exists()) { - val errorMsg = "Local $fileName file extraction failed" - listener?.onError(fileName, errorMsg) - return@withContext DownloadResult(false, - isRemoteSource = false, - errorMessage = errorMsg - ) - } - - if (!validateDownloadedFile(targetFile, fileName)) { - val errorMsg = "Local $fileName file verification failed" - listener?.onError(fileName, errorMsg) - return@withContext DownloadResult( - success = false, - isRemoteSource = false, - errorMessage = errorMsg - ) - } - - Log.i(TAG, "$fileName local version loaded successfully, file size: ${targetFile.length()} bytes") - listener?.onLog("$fileName local version loaded successfully") - listener?.onSuccess(fileName, false) - - DownloadResult(true, isRemoteSource = false) - - } catch (e: Exception) { - Log.e(TAG, "$fileName local version loading failed", e) - val errorMsg = "Local version loading failed: ${e.message}" - listener?.onError(fileName, errorMsg) - DownloadResult(success = false, isRemoteSource = false, errorMessage = errorMsg) - } - } - - private fun validateDownloadedFile(file: File, fileName: String): Boolean { - if (!file.exists()) { - Log.w(TAG, "$fileName file does not exist") - return false - } - - val fileSize = file.length() - if (fileSize < MIN_FILE_SIZE) { - Log.w(TAG, "$fileName file is too small: $fileSize bytes") - return false - } - - try { - file.inputStream().use { input -> - val header = ByteArray(4) - val bytesRead = input.read(header) - - if (bytesRead < 4) { - Log.w(TAG, "$fileName file header read incomplete") - return false - } - - val isELF = header[0] == 0x7F.toByte() && - header[1] == 'E'.code.toByte() && - header[2] == 'L'.code.toByte() && - header[3] == 'F'.code.toByte() - - if (fileName == "kptools" && !isELF) { - Log.w(TAG, "kptools file format is invalid, not ELF format") - return false - } - - Log.d(TAG, "$fileName file verification passed, size: $fileSize bytes, ELF: $isELF") - return true - } - } catch (e: Exception) { - Log.w(TAG, "$fileName file verification exception", e) - return false - } - } - - private fun setExecutablePermission(filePath: String) { - try { - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "chmod a+rx $filePath")) - process.waitFor() - Log.d(TAG, "Set execution permission for $filePath") - } catch (e: Exception) { - Log.w(TAG, "Failed to set execution permission: $filePath", e) - try { - File(filePath).setExecutable(true, false) - } catch (ex: Exception) { - Log.w(TAG, "Java method to set permissions also failed", ex) - } - } - } - - - fun cleanup() { - try { - File(workDir).listFiles()?.forEach { file -> - if (file.name.endsWith(".tmp")) { - file.delete() - Log.d(TAG, "Cleaned temporary file: ${file.name}") - } - } - } catch (e: Exception) { - Log.w(TAG, "Failed to clean temporary files", e) - } - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt index 39201e5c..9d4f9496 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt @@ -1,75 +1,71 @@ package com.sukisu.ultra.ui -import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageInfo -import android.os.* +import android.content.pm.PackageManager +import android.os.IBinder +import android.os.UserHandle +import android.os.UserManager import android.util.Log import com.topjohnwu.superuser.ipc.RootService import com.sukisu.zako.IKsuInterface +import rikka.parcelablelist.ParcelableListSlice /** - * @author ShirkNeko - * @date 2025/10/17. + * @author weishu + * @date 2023/4/18. */ + class KsuService : RootService() { - private val TAG = "KsuService" - - private val cacheLock = Object() - private var _all: List? = null - private val allPackages: List - get() = synchronized(cacheLock) { - _all ?: loadAllPackages().also { _all = it } - } - - private fun loadAllPackages(): List { - val tmp = arrayListOf() - for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) { - val userId = user.getUserIdCompat() - tmp += getInstalledPackagesAsUser(userId) - } - return tmp + companion object { + private const val TAG = "KsuService" } - internal inner class Stub : IKsuInterface.Stub() { - override fun getPackageCount(): Int = allPackages.size - - override fun getPackages(start: Int, maxCount: Int): List { - val list = allPackages - val end = (start + maxCount).coerceAtMost(list.size) - return if (start >= list.size) emptyList() - else list.subList(start, end) - } + override fun onBind(intent: Intent): IBinder { + return Stub() } - override fun onBind(intent: Intent): IBinder = Stub() + private fun getUserIds(): List { + val result = ArrayList() + val um = getSystemService(USER_SERVICE) as UserManager + val userProfiles = um.userProfiles + for (userProfile: UserHandle in userProfiles) { + result.add(userProfile.hashCode()) + } + return result + } - @SuppressLint("PrivateApi") - private fun getInstalledPackagesAsUser(userId: Int): List { + private fun getInstalledPackagesAll(flags: Int): ArrayList { + val packages = ArrayList() + for (userId in getUserIds()) { + Log.i(TAG, "getInstalledPackagesAll: $userId") + packages.addAll(getInstalledPackagesAsUser(flags, userId)) + } + return packages + } + + @Suppress("UNCHECKED_CAST") + private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List { return try { - val pm = packageManager - val m = pm.javaClass.getDeclaredMethod( + val pm: PackageManager = packageManager + val method = pm.javaClass.getDeclaredMethod( "getInstalledPackagesAsUser", - Int::class.java, - Int::class.java + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType ) - @Suppress("UNCHECKED_CAST") - m.invoke(pm, 0, userId) as List + method.invoke(pm, flags, userId) as List } catch (e: Throwable) { - Log.e(TAG, "getInstalledPackagesAsUser", e) - emptyList() + Log.e(TAG, "err", e) + ArrayList() } } - private fun UserHandle.getUserIdCompat(): Int { - return try { - javaClass.getDeclaredField("identifier").apply { isAccessible = true }.getInt(this) - } catch (_: NoSuchFieldException) { - javaClass.getDeclaredMethod("getIdentifier").invoke(this) as Int - } catch (e: Throwable) { - Log.e("KsuService", "getUserIdCompat", e) - 0 + private inner class Stub : IKsuInterface.Stub() { + override fun getPackages(flags: Int): ParcelableListSlice { + val list = getInstalledPackagesAll(flags) + Log.i(TAG, "getPackages: ${list.size}") + return ParcelableListSlice(list) } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt index 033ee447..4ccff11e 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt @@ -1,307 +1,169 @@ package com.sukisu.ultra.ui -import android.content.Context -import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope import androidx.navigation.NavBackStackEntry -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination -import com.ramcosta.composedestinations.spec.NavHostGraphSpec -import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator -import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.screen.BottomBarDestination -import com.sukisu.ultra.ui.theme.KernelSUTheme -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.install -import com.sukisu.ultra.ui.viewmodel.HomeViewModel -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import com.sukisu.ultra.ui.webui.initPlatform -import com.sukisu.ultra.ui.component.* -import kotlinx.coroutines.flow.MutableStateFlow +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.launch -import com.sukisu.ultra.ui.activity.component.BottomBar -import com.sukisu.ultra.ui.activity.util.* +import com.sukisu.ultra.Natives +import com.sukisu.ultra.ui.component.BottomBar +import com.sukisu.ultra.ui.screen.HomePager +import com.sukisu.ultra.ui.screen.ModulePager +import com.sukisu.ultra.ui.screen.SettingPager +import com.sukisu.ultra.ui.screen.SuperUserPager +import com.sukisu.ultra.ui.theme.KernelSUTheme +import com.sukisu.ultra.ui.util.install +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.theme.MiuixTheme class MainActivity : ComponentActivity() { - private lateinit var superUserViewModel: SuperUserViewModel - private lateinit var homeViewModel: HomeViewModel - internal val settingsStateFlow = MutableStateFlow(SettingsState()) - - data class SettingsState( - val isHideOtherInfo: Boolean = false, - val showKpmInfo: Boolean = false - ) - - private var showConfirmationDialog = mutableStateOf(false) - private var pendingZipFiles = mutableStateOf>(emptyList()) - - private lateinit var themeChangeObserver: ThemeChangeContentObserver - private var isInitialized = false - - override fun attachBaseContext(newBase: Context?) { - super.attachBaseContext(newBase?.let { LocaleHelper.applyLanguage(it) }) - } override fun onCreate(savedInstanceState: Bundle?) { - try { - // 应用自定义 DPI - DisplayUtils.applyCustomDpi(this) - // Enable edge to edge - enableEdgeToEdge() + // Enable edge to edge + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } + super.onCreate(savedInstanceState) - super.onCreate(savedInstanceState) + val isManager = Natives.isManager + if (isManager && !Natives.requireNewKernel()) install() - val isManager = Natives.isManager - if (isManager && !Natives.requireNewKernel()) { - install() - } + setContent { + KernelSUTheme { + val navController = rememberNavController() - // 使用标记控制初始化流程 - if (!isInitialized) { - initializeViewModels() - initializeData() - isInitialized = true - } + Scaffold { + DestinationsNavHost( + modifier = Modifier, + navGraph = NavGraphs.root, + navController = navController, + defaultTransitions = object : NavHostAnimatedDestinationStyle() { + override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = + { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) + } - // Check if launched with a ZIP file - val zipUri: ArrayList? = when (intent?.action) { - Intent.ACTION_SEND -> { - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(Intent.EXTRA_STREAM) - } - uri?.let { arrayListOf(it) } - } + override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition = + { + slideOutHorizontally( + targetOffsetX = { -it / 5 }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) + } - Intent.ACTION_SEND_MULTIPLE -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) - } - } + override val popEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = + { + slideInHorizontally( + initialOffsetX = { -it / 5 }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) + } - else -> when { - intent?.data != null -> arrayListOf(intent.data!!) - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { - intent.getParcelableArrayListExtra("uris", Uri::class.java) - } - else -> { - @Suppress("DEPRECATION") - intent.getParcelableArrayListExtra("uris") - } - } - } - - setContent { - KernelSUTheme { - val navController = rememberNavController() - val snackBarHostState = remember { SnackbarHostState() } - val currentDestination = navController.currentBackStackEntryAsState().value?.destination - - val bottomBarRoutes = remember { - BottomBarDestination.entries.map { it.direction.route }.toSet() - } - - val navigator = navController.rememberDestinationsNavigator() - - InstallConfirmationDialog( - show = showConfirmationDialog.value, - zipFiles = pendingZipFiles.value, - onConfirm = { confirmedFiles -> - showConfirmationDialog.value = false - UltraActivityUtils.navigateToFlashScreen(this, confirmedFiles, navigator) - }, - onDismiss = { - showConfirmationDialog.value = false - pendingZipFiles.value = emptyList() - finish() + override val popExitTransition: AnimatedContentTransitionScope.() -> ExitTransition = + { + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) + } } ) - - LaunchedEffect(zipUri) { - if (!zipUri.isNullOrEmpty()) { - // 检测 ZIP 文件类型并显示确认对话框 - lifecycleScope.launch { - UltraActivityUtils.detectZipTypeAndShowConfirmation(this@MainActivity, zipUri) { infos -> - if (infos.isNotEmpty()) { - pendingZipFiles.value = infos - showConfirmationDialog.value = true - } else { - finish() - } - } - } - } - } - - val showBottomBar = when (currentDestination?.route) { - ExecuteModuleActionScreenDestination.route -> false - else -> true - } - - LaunchedEffect(Unit) { - initPlatform() - } - - CompositionLocalProvider( - LocalSnackbarHost provides snackBarHostState - ) { - Scaffold( - bottomBar = { - AnimatedBottomBar.AnimatedBottomBarWrapper( - showBottomBar = showBottomBar, - content = { BottomBar(navController) } - ) - }, - contentWindowInsets = WindowInsets(0, 0, 0, 0) - ) { innerPadding -> - DestinationsNavHost( - modifier = Modifier.padding(innerPadding), - navGraph = NavGraphs.root as NavHostGraphSpec, - navController = navController, - defaultTransitions = object : NavHostAnimatedDestinationStyle() { - override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { - // If the target is a detail page (not a bottom navigation page), slide in from the right - if (targetState.destination.route !in bottomBarRoutes) { - slideInHorizontally(initialOffsetX = { it }) - } else { - // Otherwise (switching between bottom navigation pages), use fade in - fadeIn(animationSpec = tween(340)) - } - } - - override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition = { - // If navigating from the home page (bottom navigation page) to a detail page, slide out to the left - if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) { - slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut() - } else { - // Otherwise (switching between bottom navigation pages), use fade out - fadeOut(animationSpec = tween(340)) - } - } - - override val popEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { - // If returning to the home page (bottom navigation page), slide in from the left - if (targetState.destination.route in bottomBarRoutes) { - slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn() - } else { - // Otherwise (e.g., returning between multiple detail pages), use default fade in - fadeIn(animationSpec = tween(340)) - } - } - - override val popExitTransition: AnimatedContentTransitionScope.() -> ExitTransition = { - // If returning from a detail page (not a bottom navigation page), scale down and fade out - if (initialState.destination.route !in bottomBarRoutes) { - scaleOut(targetScale = 0.9f) + fadeOut() - } else { - // Otherwise, use default fade out - fadeOut(animationSpec = tween(340)) - } - } - } - ) - } - } } } - } catch (e: Exception) { - e.printStackTrace() + } + } +} + + +val LocalPagerState = compositionLocalOf { error("No pager state") } +val LocalHandlePageChange = compositionLocalOf<(Int) -> Unit> { error("No handle page change") } + +@Composable +@Destination(start = true) +fun MainScreen(navController: DestinationsNavigator) { + val activity = LocalActivity.current + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 4 }) + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = MiuixTheme.colorScheme.background, + tint = HazeTint(MiuixTheme.colorScheme.background.copy(0.8f)) + ) + val handlePageChange: (Int) -> Unit = remember(pagerState, coroutineScope) { + { page -> + coroutineScope.launch { pagerState.animateScrollToPage(page) } } } - private fun initializeViewModels() { - superUserViewModel = SuperUserViewModel() - homeViewModel = HomeViewModel() - - // 设置主题变化监听器 - themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this) - } - - private fun initializeData() { - lifecycleScope.launch { - try { - superUserViewModel.fetchAppList() - } catch (e: Exception) { - e.printStackTrace() + BackHandler { + if (pagerState.currentPage != 0) { + coroutineScope.launch { + pagerState.animateScrollToPage(0) } - } - - // 数据刷新协程 - DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope) - DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow) - - // 初始化主题相关设置 - ThemeUtils.initializeThemeSettings(this, settingsStateFlow) - } - - override fun onResume() { - try { - super.onResume() - ThemeUtils.onActivityResume() - - // 仅在需要时刷新数据 - if (isInitialized) { - refreshData() - } - } catch (e: Exception) { - e.printStackTrace() + } else { + activity?.finishAndRemoveTask() } } - private fun refreshData() { - lifecycleScope.launch { - try { - superUserViewModel.fetchAppList() - DataRefreshUtils.refreshData(lifecycleScope) - } catch (e: Exception) { - e.printStackTrace() + CompositionLocalProvider( + LocalPagerState provides pagerState, + LocalHandlePageChange provides handlePageChange + ) { + Scaffold( + bottomBar = { + BottomBar(hazeState, hazeStyle) + }, + ) { innerPadding -> + HorizontalPager( + modifier = Modifier.hazeSource(state = hazeState), + state = pagerState, + beyondViewportPageCount = 2, + userScrollEnabled = false + ) { + when (it) { + 0 -> HomePager(pagerState, navController, innerPadding.calculateBottomPadding()) + 1 -> SuperUserPager(navController, innerPadding.calculateBottomPadding()) + 2 -> ModulePager(navController, innerPadding.calculateBottomPadding()) + 3 -> SettingPager(navController, innerPadding.calculateBottomPadding()) + } } } } - - override fun onPause() { - try { - super.onPause() - ThemeUtils.onActivityPause(this) - } catch (e: Exception) { - e.printStackTrace() - } - } - - override fun onDestroy() { - try { - ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver) - super.onDestroy() - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt deleted file mode 100644 index 7efffade..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.sukisu.ultra.ui.activity.component - -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavHostController -import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.spec.RouteOrDirection -import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState -import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.MainActivity -import com.sukisu.ultra.ui.activity.util.* -import com.sukisu.ultra.ui.activity.util.AppData.getKpmVersionUse -import com.sukisu.ultra.ui.screen.BottomBarDestination -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.util.* - -@SuppressLint("ContextCastToActivity") -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BottomBar(navController: NavHostController) { - val navigator = navController.rememberDestinationsNavigator() - val isFullFeatured = AppData.isFullFeatured() - val kpmVersion = getKpmVersionUse() - val cardColor = MaterialTheme.colorScheme.surfaceContainer - val activity = LocalContext.current as MainActivity - val settings by activity.settingsStateFlow.collectAsState() - - // 检查是否隐藏红点 - val isHideOtherInfo = settings.isHideOtherInfo - val showKpmInfo = settings.showKpmInfo - - // 收集计数数据 - val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState() - val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState() - val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState() - - - NavigationBar( - modifier = Modifier.windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) - ), - containerColor = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ).containerColor, - tonalElevation = cardElevation - ) { - BottomBarDestination.entries.forEach { destination -> - if (destination == BottomBarDestination.Kpm) { - if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && !showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { - if (!isFullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (!isCurrentDestOnBackStack) { - navigator.popBackStack(destination.direction, false) - } - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root as RouteOrDirection) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - BadgedBox( - badge = { - if (kpmModuleCount > 0 && !isHideOtherInfo) { - Badge( - containerColor = MaterialTheme.colorScheme.secondary - ) { - Text( - text = kpmModuleCount.toString(), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - ) { - if (isCurrentDestOnBackStack) { - Icon(destination.iconSelected, stringResource(destination.label)) - } else { - Icon(destination.iconNotSelected, stringResource(destination.label)) - } - } - }, - label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) }, - alwaysShowLabel = false - ) - } - } else if (destination == BottomBarDestination.SuperUser) { - if (!isFullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (isCurrentDestOnBackStack) { - navigator.popBackStack(destination.direction, false) - } - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - BadgedBox( - badge = { - if (superuserCount > 0 && !isHideOtherInfo) { - Badge( - containerColor = MaterialTheme.colorScheme.secondary - ) { - Text( - text = superuserCount.toString(), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - ) { - if (isCurrentDestOnBackStack) { - Icon(destination.iconSelected, stringResource(destination.label)) - } else { - Icon(destination.iconNotSelected, stringResource(destination.label)) - } - } - }, - label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) }, - alwaysShowLabel = false - ) - } else if (destination == BottomBarDestination.Module) { - if (!isFullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (isCurrentDestOnBackStack) { - navigator.popBackStack(destination.direction, false) - } - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - BadgedBox( - badge = { - if (moduleCount > 0 && !isHideOtherInfo) { - Badge( - containerColor = MaterialTheme.colorScheme.secondary) - { - Text( - text = moduleCount.toString(), - style = MaterialTheme.typography.labelSmall - ) - } - } - } - ) { - if (isCurrentDestOnBackStack) { - Icon(destination.iconSelected, stringResource(destination.label)) - } else { - Icon(destination.iconNotSelected, stringResource(destination.label)) - } - } - }, - label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) }, - alwaysShowLabel = false - ) - } else { - if (!isFullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (isCurrentDestOnBackStack) { - navigator.popBackStack(destination.direction, false) - } - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - if (isCurrentDestOnBackStack) { - Icon(destination.iconSelected, stringResource(destination.label)) - } else { - Icon(destination.iconNotSelected, stringResource(destination.label)) - } - }, - label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) }, - alwaysShowLabel = false - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt deleted file mode 100644 index 6786917e..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.sukisu.ultra.ui.activity.util - -import android.content.Context -import android.database.ContentObserver -import android.os.Handler -import android.provider.Settings -import androidx.core.content.edit -import com.sukisu.ultra.ui.MainActivity -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.ThemeConfig -import kotlinx.coroutines.flow.MutableStateFlow - -class ThemeChangeContentObserver( - handler: Handler, - private val onThemeChanged: () -> Unit -) : ContentObserver(handler) { - override fun onChange(selfChange: Boolean) { - super.onChange(selfChange) - onThemeChanged() - } -} - -object ThemeUtils { - - fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow) { - val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) - val isFirstRun = prefs.getBoolean("is_first_run", true) - - settingsStateFlow.value = MainActivity.SettingsState( - isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), - showKpmInfo = prefs.getBoolean("show_kpm_info", false) - ) - - if (isFirstRun) { - ThemeConfig.preventBackgroundRefresh = false - activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { - putBoolean("prevent_background_refresh", false) - } - prefs.edit { putBoolean("is_first_run", false) } - } - - // 加载保存的背景设置 - loadThemeMode() - loadThemeColors() - loadDynamicColorState() - CardConfig.load(activity.applicationContext) - } - - fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver { - val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) { - activity.runOnUiThread { - if (!ThemeConfig.preventBackgroundRefresh) { - ThemeConfig.backgroundImageLoaded = false - loadCustomBackground() - } - } - } - - activity.contentResolver.registerContentObserver( - Settings.System.getUriFor("ui_night_mode"), - false, - contentObserver - ) - - return contentObserver - } - - fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) { - activity.contentResolver.unregisterContentObserver(observer) - } - - fun onActivityPause(activity: MainActivity) { - CardConfig.save(activity.applicationContext) - activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { - putBoolean("prevent_background_refresh", true) - } - ThemeConfig.preventBackgroundRefresh = true - } - - fun onActivityResume() { - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - loadCustomBackground() - } - } - - private fun loadThemeMode() { - } - - private fun loadThemeColors() { - } - - private fun loadDynamicColorState() { - } - - private fun loadCustomBackground() { - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt deleted file mode 100644 index 367e791a..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.sukisu.ultra.ui.activity.util - -import android.content.Context -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.runtime.Composable -import androidx.lifecycle.LifecycleCoroutineScope -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.MainActivity -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import java.util.* -import android.net.Uri -import androidx.lifecycle.lifecycleScope -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.ui.component.ZipFileDetector -import com.sukisu.ultra.ui.component.ZipFileInfo -import com.sukisu.ultra.ui.component.ZipType -import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination -import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination -import com.sukisu.ultra.ui.screen.FlashIt -import kotlinx.coroutines.withContext -import androidx.core.content.edit - -object AnimatedBottomBar { - @Composable - fun AnimatedBottomBarWrapper( - showBottomBar: Boolean, - content: @Composable () -> Unit - ) { - AnimatedVisibility( - visible = showBottomBar, - enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), - exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() - ) { - content() - } - } -} - -object UltraActivityUtils { - - suspend fun detectZipTypeAndShowConfirmation( - activity: MainActivity, - zipUris: ArrayList, - onResult: (List) -> Unit - ) { - val infos = ZipFileDetector.detectAndParseZipFiles(activity, zipUris) - withContext(Dispatchers.Main) { onResult(infos) } - } - - fun navigateToFlashScreen( - activity: MainActivity, - zipFiles: List, - navigator: DestinationsNavigator - ) { - activity.lifecycleScope.launch { - val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri } - val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri } - - when { - kernelUris.isNotEmpty() && moduleUris.isEmpty() -> { - if (kernelUris.size == 1 && rootAvailable()) { - navigator.navigate( - InstallScreenDestination( - preselectedKernelUri = kernelUris.first().toString() - ) - ) - } - setAutoExitAfterFlash(activity) - } - - moduleUris.isNotEmpty() -> { - navigator.navigate( - FlashScreenDestination( - FlashIt.FlashModules(ArrayList(moduleUris)) - ) - ) - setAutoExitAfterFlash(activity) - } - } - } - } - - private fun setAutoExitAfterFlash(activity: Context) { - activity.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean("auto_exit_after_flash", true) - } - } -} - -object AppData { - object DataRefreshManager { - // 私有状态流 - private val _superuserCount = MutableStateFlow(0) - private val _moduleCount = MutableStateFlow(0) - private val _kpmModuleCount = MutableStateFlow(0) - - // 公开的只读状态流 - val superuserCount: StateFlow = _superuserCount.asStateFlow() - val moduleCount: StateFlow = _moduleCount.asStateFlow() - val kpmModuleCount: StateFlow = _kpmModuleCount.asStateFlow() - - /** - * 刷新所有数据计数 - */ - fun refreshData() { - _superuserCount.value = getSuperuserCountUse() - _moduleCount.value = getModuleCountUse() - _kpmModuleCount.value = getKpmModuleCountUse() - } - } - - /** - * 获取超级用户应用计数 - */ - fun getSuperuserCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getSuperuserCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取模块计数 - */ - fun getModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM模块计数 - */ - fun getKpmModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - val kpmVersion = getKpmVersionUse() - if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0 - getKpmModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM版本 - */ - fun getKpmVersionUse(): String { - return try { - if (!rootAvailable()) return "" - val version = getKpmVersion() - version.ifEmpty { "" } - } catch (e: Exception) { - "Error: ${e.message}" - } - } - - /** - * 检查是否是完整功能模式 - */ - fun isFullFeatured(): Boolean { - val isManager = Natives.isManager - return isManager && !Natives.requireNewKernel() && rootAvailable() - } -} - -object DataRefreshUtils { - fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) { - scope.launch(Dispatchers.IO) { - while (isActive) { - AppData.DataRefreshManager.refreshData() - delay(5000) - } - } - } - - fun startSettingsMonitorCoroutine( - scope: LifecycleCoroutineScope, - activity: MainActivity, - settingsStateFlow: MutableStateFlow - ) { - scope.launch(Dispatchers.IO) { - while (isActive) { - val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) - settingsStateFlow.value = MainActivity.SettingsState( - isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), - showKpmInfo = prefs.getBoolean("show_kpm_info", false) - ) - delay(1000) - } - } - } - - fun refreshData(scope: LifecycleCoroutineScope) { - scope.launch { - AppData.DataRefreshManager.refreshData() - } - } -} - -object DisplayUtils { - fun applyCustomDpi(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val customDpi = prefs.getInt("app_dpi", 0) - - if (customDpi > 0) { - try { - val resources = context.resources - val metrics = resources.displayMetrics - metrics.density = customDpi / 160f - @Suppress("DEPRECATION") - metrics.scaledDensity = customDpi / 160f - metrics.densityDpi = customDpi - } catch (e: Exception) { - e.printStackTrace() - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt deleted file mode 100644 index 5dcda95f..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.* -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.R - -@Preview -@Composable -fun AboutCard() { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - AboutCardContent() - } - } -} - -@Composable -fun AboutDialog(dismiss: () -> Unit) { - Dialog( - onDismissRequest = { dismiss() } - ) { - AboutCard() - } -} - -@Composable -private fun AboutCardContent() { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Row { - Surface( - modifier = Modifier.size(40.dp), - color = colorResource(id = R.color.ic_launcher_background), - shape = CircleShape - ) { - Image( - painter = painterResource(id = R.drawable.ic_launcher_monochrome), - contentDescription = "icon", - modifier = Modifier.scale(1.4f) - ) - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column { - - Text( - stringResource(id = R.string.app_name), - style = MaterialTheme.typography.titleSmall, - fontSize = 18.sp - ) - Text( - BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.bodySmall, - fontSize = 14.sp - ) - - Spacer(modifier = Modifier.height(8.dp)) - - val annotatedString = AnnotatedString.fromHtml( - htmlString = stringResource( - id = R.string.about_source_code, - "GitHub", - "Telegram", - "怡子曰曰", - "明风 OuO", - "CC BY-NC-SA 4.0" - ), - linkStyles = TextLinkStyles( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - pressedStyle = SpanStyle( - color = MaterialTheme.colorScheme.primary, - background = MaterialTheme.colorScheme.secondaryContainer, - textDecoration = TextDecoration.Underline - ) - ) - ) - Text( - text = annotatedString, - style = TextStyle( - fontSize = 14.sp - ) - ) - } - } - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/AppIconImage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/AppIconImage.kt new file mode 100644 index 00000000..275403f8 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/AppIconImage.kt @@ -0,0 +1,57 @@ +package com.sukisu.ultra.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.kyant.capsule.ContinuousRoundedRectangle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme + +@Composable +fun AppIconImage( + packageInfo: PackageInfo, + label: String, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + var icon by remember(packageInfo.packageName) { mutableStateOf(null) } + + LaunchedEffect(packageInfo.packageName) { + withContext(Dispatchers.IO) { + val drawable = packageInfo.applicationInfo?.loadIcon(context.packageManager) + val bitmap = drawable?.toBitmap()?.asImageBitmap() + icon = bitmap + } + } + + icon.let { imageBitmap -> + imageBitmap?.let { + Image( + bitmap = it, + contentDescription = label, + modifier = modifier + ) + } + } ?: Box( + modifier = modifier + .clip(ContinuousRoundedRectangle(12.dp)) + .background(colorScheme.secondaryContainer), + contentAlignment = Alignment.Center + ) {} +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/BottomBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/BottomBar.kt new file mode 100644 index 00000000..186d9763 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/BottomBar.kt @@ -0,0 +1,69 @@ +package com.sukisu.ultra.ui.component + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Cottage +import androidx.compose.material.icons.rounded.Extension +import androidx.compose.material.icons.rounded.Security +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.hazeEffect +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.LocalHandlePageChange +import com.sukisu.ultra.ui.LocalPagerState +import com.sukisu.ultra.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.NavigationBar +import top.yukonga.miuix.kmp.basic.NavigationItem + + +@Composable +fun BottomBar( + hazeState: HazeState, + hazeStyle: HazeStyle +) { + val isManager = Natives.isManager + val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() + + val page = LocalPagerState.current.targetPage + val handlePageChange = LocalHandlePageChange.current + + if (!fullFeatured) return + + val item = BottomBarDestination.entries.mapIndexed { index, destination -> + NavigationItem( + label = stringResource(destination.label), + icon = destination.icon, + ) + } + + NavigationBar( + modifier = Modifier + .hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + items = item, + selected = page, + onClick = handlePageChange + ) +} + +enum class BottomBarDestination( + @get:StringRes val label: Int, + val icon: ImageVector, +) { + Home(R.string.home, Icons.Rounded.Cottage), + SuperUser(R.string.superuser, Icons.Rounded.Security), + Module(R.string.module, Icons.Rounded.Extension), + Setting(R.string.settings, Icons.Rounded.Settings) +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ChooseKmiDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ChooseKmiDialog.kt new file mode 100644 index 00000000..e39f4505 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ChooseKmiDialog.kt @@ -0,0 +1,74 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.util.getSupportedKmis +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme + +@Composable +fun ChooseKmiDialog( + showDialog: MutableState, + onSelected: (String?) -> Unit +) { + val supportedKmi by produceState(initialValue = emptyList()) { + value = getSupportedKmis() + } + val options = supportedKmi.map { it } + + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.select_kmi), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = colorScheme.onSurface + ) + options.forEachIndexed { index, type -> + SuperArrow( + title = type, + onClick = { + onSelected(type) + showDialog.value = false + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + } + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt index 10c04772..cc9448a9 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt @@ -7,45 +7,66 @@ import android.text.Layout import android.text.method.LinkMovementMethod import android.util.Log import android.view.ViewGroup +import android.widget.ScrollView import android.widget.TextView -import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.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.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.Layout import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import io.noties.markwon.Markwon import io.noties.markwon.utils.NoCopySpannableFactory -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.parcelize.Parcelize +import com.sukisu.ultra.R +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme import kotlin.coroutines.resume private const val TAG = "DialogComponent" interface ConfirmDialogVisuals : Parcelable { val title: String - val content: String + val content: String? val isMarkdown: Boolean val confirm: String? val dismiss: String? @@ -54,7 +75,7 @@ interface ConfirmDialogVisuals : Parcelable { @Parcelize private data class ConfirmDialogVisualsImpl( override val title: String, - override val content: String, + override val content: String?, override val isMarkdown: Boolean, override val confirm: String?, override val dismiss: String?, @@ -86,16 +107,15 @@ interface ConfirmDialogHandle : DialogHandle { fun showConfirm( title: String, - content: String, + content: String? = null, markdown: Boolean = false, confirm: String? = null, dismiss: String? = null ) suspend fun awaitConfirm( - title: String, - content: String, + content: String? = null, markdown: Boolean = false, confirm: String? = null, dismiss: String? = null @@ -159,7 +179,10 @@ interface ConfirmCallback { val isEmpty: Boolean get() = onConfirm == null && onDismiss == null companion object { - operator fun invoke(onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback): ConfirmCallback { + operator fun invoke( + onConfirmProvider: () -> NullableCallback, + onDismissProvider: () -> NullableCallback + ): ConfirmCallback { return object : ConfirmCallback { override val onConfirm: NullableCallback get() = onConfirmProvider() @@ -250,7 +273,7 @@ private class ConfirmDialogHandleImpl( override fun showConfirm( title: String, - content: String, + content: String?, markdown: Boolean, confirm: String?, dismiss: String? @@ -263,7 +286,7 @@ private class ConfirmDialogHandleImpl( override suspend fun awaitConfirm( title: String, - content: String, + content: String?, markdown: Boolean, confirm: String?, dismiss: String? @@ -299,23 +322,12 @@ private class ConfirmDialogHandleImpl( } } -private class CustomDialogHandleImpl( - visible: MutableState, - coroutineScope: CoroutineScope -) : DialogHandleBase(visible, coroutineScope) { - override val dialogType: String get() = "CustomDialog" -} - @Composable fun rememberLoadingDialog(): LoadingDialogHandle { - val visible = remember { - mutableStateOf(false) - } + val visible = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() - if (visible.value) { - LoadingDialog() - } + LoadingDialog(visible) return remember { LoadingDialogHandleImpl(visible, coroutineScope) @@ -343,7 +355,8 @@ private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: Confi ConfirmDialog( handle.visuals, confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, - dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } } + dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }, + showDialog = visible ) } @@ -370,99 +383,130 @@ fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { } @Composable -fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle { - val visible = rememberSaveable { - mutableStateOf(false) - } - val coroutineScope = rememberCoroutineScope() - if (visible.value) { - composable { visible.value = false } - } - return remember { - CustomDialogHandleImpl(visible, coroutineScope) - } -} - -@Composable -private fun LoadingDialog() { - Dialog( +private fun LoadingDialog(showDialog: MutableState) { + SuperDialog( + show = showDialog, onDismissRequest = {}, - properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) - ) { - Surface( - modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) - ) { + content = { Box( - contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart ) { - CircularProgressIndicator() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + InfiniteProgressIndicator( + color = MiuixTheme.colorScheme.onBackground + ) + Text( + modifier = Modifier.padding(start = 12.dp), + text = stringResource(R.string.processing), + fontWeight = FontWeight.Medium + ) + } } } - } + ) } @Composable -private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) { - AlertDialog( +private fun ConfirmDialog( + visuals: ConfirmDialogVisuals, + confirm: () -> Unit, + dismiss: () -> Unit, + showDialog: MutableState +) { + SuperDialog( + modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)), + show = showDialog, + title = visuals.title, onDismissRequest = { dismiss() + showDialog.value = false }, - title = { - Text(text = visuals.title) - }, - text = { - if (visuals.isMarkdown) { - MarkdownContent(content = visuals.content) - } else { - Text(text = visuals.content) + content = { + Layout( + content = { + visuals.content?.let { + if (visuals.isMarkdown) { + MarkdownContent(content = visuals.content!!) + } else { + Text(text = visuals.content!!) + } + } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.padding(top = 12.dp) + ) { + TextButton( + text = visuals.dismiss ?: stringResource(id = android.R.string.cancel), + onClick = { + dismiss() + showDialog.value = false + }, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(20.dp)) + TextButton( + text = visuals.confirm ?: stringResource(id = android.R.string.ok), + onClick = { + confirm() + showDialog.value = false + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } + ) { measurables, constraints -> + if (measurables.size != 2) { + val button = measurables[0].measure(constraints) + layout(constraints.maxWidth, button.height) { + button.place(0, 0) + } + } else { + val button = measurables[1].measure(constraints) + val lazyList = measurables[0].measure(constraints.copy(maxHeight = constraints.maxHeight - button.height)) + layout(constraints.maxWidth, lazyList.height + button.height) { + lazyList.place(0, 0) + button.place(0, lazyList.height) + } + } } - }, - confirmButton = { - TextButton(onClick = confirm) { - Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = dismiss) { - Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) - } - }, + } ) } @Composable private fun MarkdownContent(content: String) { - val contentColor = LocalContentColor.current - val scrollState = rememberScrollState() + val contentColor = MiuixTheme.colorScheme.onBackground.toArgb() - Column( + AndroidView( + factory = { context -> + val scrollView = ScrollView(context) + val textView = TextView(context).apply { + movementMethod = LinkMovementMethod.getInstance() + setSpannableFactory(NoCopySpannableFactory.getInstance()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE + } + hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + scrollView.addView(textView) + scrollView + }, modifier = Modifier .fillMaxWidth() - .verticalScroll( - state = scrollState, - flingBehavior = ScrollableDefaults.flingBehavior() - ) - .padding(12.dp) - ) { - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethod.getInstance() - setSpannableFactory(NoCopySpannableFactory.getInstance()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE - } - hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - }, - update = { - Markwon.create(it.context).setMarkdown(it, content) - it.setTextColor(contentColor.toArgb()) - } - ) - } + .wrapContentHeight() + .clipToBounds(), + update = { + val textView = it.getChildAt(0) as TextView + Markwon.create(textView.context).setMarkdown(textView, content) + textView.setTextColor(contentColor) + } + ) } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/DropdownItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/DropdownItem.kt new file mode 100644 index 00000000..dbf8cc41 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/DropdownItem.kt @@ -0,0 +1,46 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun DropdownItem( + text: String, + optionSize: Int, + index: Int, + dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), + onSelectedIndexChange: (Int) -> Unit +) { + val currentOnSelectedIndexChange = rememberUpdatedState(onSelectedIndexChange) + val additionalTopPadding = if (index == 0) 20f.dp else 12f.dp + val additionalBottomPadding = if (index == optionSize - 1) 20f.dp else 12f.dp + + Row( + modifier = Modifier + .clickable { currentOnSelectedIndexChange.value(index) } + .background(dropdownColors.containerColor) + .padding(horizontal = 20.dp) + .padding( + top = additionalTopPadding, + bottom = additionalBottomPadding + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + fontSize = MiuixTheme.textStyles.body1.fontSize, + fontWeight = FontWeight.Medium, + color = dropdownColors.contentColor, + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/EditText.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/EditText.kt new file mode 100644 index 00000000..045aed5f --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/EditText.kt @@ -0,0 +1,199 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import kotlin.math.max + +@Composable +fun EditText( + title: String, + summary: String? = null, + textValue: MutableState, + onTextValueChange: (String) -> Unit = {}, + textHint: String = "", + enabled: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + titleColor: BasicComponentColors = EditTextDefaults.titleColor(), + summaryColor: BasicComponentColors = EditTextDefaults.summaryColor(), + rightActionColor: BasicComponentColors = EditTextDefaults.rightActionColors(), + isError: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + val coroutineScope = rememberCoroutineScope() + val focused = interactionSource.collectIsFocusedAsState().value + val focusRequester = remember { FocusRequester() } + if (focused) { + focusRequester.requestFocus() + } + + Box( + modifier = Modifier + .clickable( + indication = null, + interactionSource = null + ) { + if (enabled) { + coroutineScope.launch { + interactionSource.emit(FocusInteraction.Focus()) + } + } + } + .heightIn(min = 56.dp) + .fillMaxWidth() + .padding(EditTextDefaults.InsideMargin), + ) { + Layout( + content = { + Text( + text = title, + fontSize = MiuixTheme.textStyles.headline1.fontSize, + fontWeight = FontWeight.Medium, + color = titleColor.color(enabled) + ) + summary?.let { + Text( + text = it, + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = summaryColor.color(enabled) + ) + } + BasicTextField( + value = textValue.value, + onValueChange = { + onTextValueChange(it) + }, + modifier = Modifier + .focusRequester(focusRequester) + .semantics { + onClick { + focusRequester.requestFocus() + true + } + }, + enabled = enabled, + textStyle = MiuixTheme.textStyles.main.copy( + textAlign = TextAlign.End, + color = if (isError) { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) + } else { + rightActionColor.color(enabled) + } + ), + keyboardOptions = keyboardOptions, + cursorBrush = SolidColor(colorScheme.primary), + interactionSource = interactionSource, + decorationBox = + @Composable { innerTextField -> + Box( + contentAlignment = Alignment.CenterEnd + ) { + Text( + text = if (textValue.value.isEmpty()) textHint else "", + color = rightActionColor.color(enabled), + textAlign = TextAlign.End, + softWrap = false, + maxLines = 1 + ) + innerTextField() + } + } + ) + } + ) { measurables, constraints -> + val leftConstraints = constraints.copy(maxWidth = constraints.maxWidth / 2) + val hasSummary = measurables.size > 2 + val titleText = measurables[0].measure(leftConstraints) + val summaryText = (if (hasSummary) measurables[1] else null)?.measure(leftConstraints) + val leftWidth = max(titleText.width, (summaryText?.width ?: 0)) + val leftHeight = titleText.height + (summaryText?.height ?: 0) + val rightWidth = constraints.maxWidth - leftWidth - 16.dp.roundToPx() + val rightConstraints = constraints.copy(maxWidth = rightWidth) + val inputField = (if (hasSummary) measurables[2] else measurables[1]).measure(rightConstraints) + val totalHeight = max(leftHeight, inputField.height) + layout(constraints.maxWidth, totalHeight) { + val titleY = (totalHeight - leftHeight) / 2 + titleText.placeRelative(0, titleY) + summaryText?.placeRelative(0, titleY + titleText.height) + inputField.placeRelative(constraints.maxWidth - inputField.width, (totalHeight - inputField.height) / 2) + } + } + } +} + +object EditTextDefaults { + val InsideMargin = PaddingValues(16.dp) + + @Composable + fun titleColor( + color: Color = colorScheme.onSurface, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } + + @Composable + fun summaryColor( + color: Color = colorScheme.onSurfaceVariantSummary, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } + + @Composable + fun rightActionColors( + color: Color = colorScheme.onSurfaceVariantActions, + disabledColor: Color = colorScheme.disabledOnSecondaryVariant, + ): BasicComponentColors { + return BasicComponentColors( + color = color, + disabledColor = disabledColor + ) + } +} + +@Immutable +class BasicComponentColors( + private val color: Color, + private val disabledColor: Color +) { + @Stable + fun color(enabled: Boolean): Color = if (enabled) color else disabledColor +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/FabVisibilityState.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/FabVisibilityState.kt deleted file mode 100644 index 9042cdd9..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/FabVisibilityState.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.annotation.SuppressLint -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.unit.dp - -@SuppressLint("AutoboxingStateCreation") -@Composable -fun rememberFabVisibilityState(listState: LazyListState): State { - var previousScrollOffset by remember { mutableStateOf(0) } - var previousIndex by remember { mutableStateOf(0) } - val fabVisible = remember { mutableStateOf(true) } - - LaunchedEffect(listState) { - snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - .collect { (index, offset) -> - if (previousIndex == 0 && previousScrollOffset == 0) { - fabVisible.value = true - } else { - val isScrollingDown = when { - index > previousIndex -> false - index < previousIndex -> true - else -> offset < previousScrollOffset - } - - fabVisible.value = isScrollingDown - } - - previousIndex = index - previousScrollOffset = offset - } - } - - return fabVisible -} - -@Composable -fun AnimatedFab( - visible: Boolean, - content: @Composable () -> Unit -) { - val scale by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ) - ) - - AnimatedVisibility( - visible = visible, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(targetScale = 0.8f) - ) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .scale(scale) - .alpha(scale) - ) { - content() - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt deleted file mode 100644 index 6ae6a475..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt +++ /dev/null @@ -1,441 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.content.Context -import android.net.Uri -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Help -import androidx.compose.material.icons.filled.Extension -import androidx.compose.material.icons.filled.GetApp -import androidx.compose.material.icons.filled.Memory -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.util.zip.ZipInputStream - -enum class ZipType { - MODULE, - KERNEL, - UNKNOWN -} - -data class ZipFileInfo( - val uri: Uri, - val type: ZipType, - val name: String = "", - val version: String = "", - val versionCode: String = "", - val author: String = "", - val description: String = "", - val kernelVersion: String = "", - val supported: String = "" -) - -object ZipFileDetector { - - fun detectZipType(context: Context, uri: Uri): ZipType { - return try { - context.contentResolver.openInputStream(uri)?.use { inputStream -> - ZipInputStream(inputStream).use { zipStream -> - var hasModuleProp = false - var hasToolsFolder = false - var hasAnykernelSh = false - - var entry = zipStream.nextEntry - while (entry != null) { - val entryName = entry.name.lowercase() - - when { - entryName == "module.prop" || entryName.endsWith("/module.prop") -> { - hasModuleProp = true - } - entryName.startsWith("tools/") || entryName == "tools" -> { - hasToolsFolder = true - } - entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> { - hasAnykernelSh = true - } - } - - zipStream.closeEntry() - entry = zipStream.nextEntry - } - - when { - hasModuleProp -> ZipType.MODULE - hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL - else -> ZipType.UNKNOWN - } - } - } ?: ZipType.UNKNOWN - } catch (e: IOException) { - e.printStackTrace() - ZipType.UNKNOWN - } - } - - fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo { - var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE) - - try { - context.contentResolver.openInputStream(uri)?.use { inputStream -> - ZipInputStream(inputStream).use { zipStream -> - var entry = zipStream.nextEntry - while (entry != null) { - if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) { - val reader = BufferedReader(InputStreamReader(zipStream)) - val props = mutableMapOf() - - var line = reader.readLine() - while (line != null) { - if (line.contains("=") && !line.startsWith("#")) { - val parts = line.split("=", limit = 2) - if (parts.size == 2) { - props[parts[0].trim()] = parts[1].trim() - } - } - line = reader.readLine() - } - - zipInfo = zipInfo.copy( - name = props["name"] ?: context.getString(R.string.unknown_module), - version = props["version"] ?: "", - versionCode = props["versionCode"] ?: "", - author = props["author"] ?: "", - description = props["description"] ?: "" - ) - break - } - zipStream.closeEntry() - entry = zipStream.nextEntry - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - - return zipInfo - } - - fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo { - var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL) - - try { - context.contentResolver.openInputStream(uri)?.use { inputStream -> - ZipInputStream(inputStream).use { zipStream -> - var entry = zipStream.nextEntry - while (entry != null) { - if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) { - val reader = BufferedReader(InputStreamReader(zipStream)) - val props = mutableMapOf() - - var inPropertiesBlock = false - var line = reader.readLine() - while (line != null) { - if (line.contains("properties()")) { - inPropertiesBlock = true - } else if (inPropertiesBlock && line.contains("'; }")) { - inPropertiesBlock = false - } else if (inPropertiesBlock) { - val propertyLine = line.trim() - if (propertyLine.contains("=") && !propertyLine.startsWith("#")) { - val parts = propertyLine.split("=", limit = 2) - if (parts.size == 2) { - val key = parts[0].trim() - val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"") - when (key) { - "kernel.string" -> props["name"] = value - "supported.versions" -> props["supported"] = value - } - } - } - } - - // 解析普通变量定义 - if (line.contains("kernel.string=") && !inPropertiesBlock) { - val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"") - props["name"] = value - } - if (line.contains("supported.versions=") && !inPropertiesBlock) { - val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"") - props["supported"] = value - } - if (line.contains("kernel.version=") && !inPropertiesBlock) { - val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"") - props["version"] = value - } - if (line.contains("kernel.author=") && !inPropertiesBlock) { - val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"") - props["author"] = value - } - - line = reader.readLine() - } - - zipInfo = zipInfo.copy( - name = props["name"] ?: context.getString(R.string.unknown_kernel), - version = props["version"] ?: "", - author = props["author"] ?: "", - supported = props["supported"] ?: "", - kernelVersion = props["version"] ?: "" - ) - break - } - zipStream.closeEntry() - entry = zipStream.nextEntry - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - - return zipInfo - } - - suspend fun detectAndParseZipFiles(context: Context, zipUris: List): List { - return withContext(Dispatchers.IO) { - val zipFileInfos = mutableListOf() - - for (uri in zipUris) { - val zipType = detectZipType(context, uri) - val zipInfo = when (zipType) { - ZipType.MODULE -> parseModuleInfo(context, uri) - ZipType.KERNEL -> parseKernelInfo(context, uri) - ZipType.UNKNOWN -> ZipFileInfo( - uri = uri, - type = ZipType.UNKNOWN, - name = context.getString(R.string.unknown_file) - ) - } - zipFileInfos.add(zipInfo) - } - - zipFileInfos.filter { it.type != ZipType.UNKNOWN } - } - } -} - -@Composable -fun InstallConfirmationDialog( - show: Boolean, - zipFiles: List, - onConfirm: (List) -> Unit, - onDismiss: () -> Unit -) { - if (show && zipFiles.isNotEmpty()) { - val context = LocalContext.current - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = if (zipFiles.any { it.type == ZipType.KERNEL }) - Icons.Default.Memory else Icons.Default.Extension, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = if (zipFiles.size == 1) { - context.getString(R.string.confirm_installation) - } else { - context.getString(R.string.confirm_multiple_installation, zipFiles.size) - }, - style = MaterialTheme.typography.headlineSmall - ) - } - }, - text = { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 400.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(zipFiles.size) { index -> - val zipFile = zipFiles[index] - InstallItemCard(zipFile = zipFile) - } - } - }, - confirmButton = { - Button( - onClick = { onConfirm(zipFiles) }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Default.GetApp, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(context.getString(R.string.install_confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text( - context.getString(android.R.string.cancel), - color = MaterialTheme.colorScheme.onSurface - ) - } - }, - modifier = Modifier.widthIn(min = 320.dp, max = 560.dp) - ) - } -} - -@Composable -fun InstallItemCard(zipFile: ZipFileInfo) { - val context = LocalContext.current - - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.elevatedCardColors( - containerColor = when (zipFile.type) { - ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f) - else -> MaterialTheme.colorScheme.surfaceVariant - } - ), - elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = when (zipFile.type) { - ZipType.MODULE -> Icons.Default.Extension - ZipType.KERNEL -> Icons.Default.Memory - else -> Icons.AutoMirrored.Filled.Help - }, - contentDescription = null, - tint = when (zipFile.type) { - ZipType.MODULE -> MaterialTheme.colorScheme.primary - ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = zipFile.name.ifEmpty { - when (zipFile.type) { - ZipType.MODULE -> context.getString(R.string.unknown_module) - ZipType.KERNEL -> context.getString(R.string.unknown_kernel) - else -> context.getString(R.string.unknown_file) - } - }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = when (zipFile.type) { - ZipType.MODULE -> context.getString(R.string.module_package) - ZipType.KERNEL -> context.getString(R.string.kernel_package) - else -> context.getString(R.string.unknown_package) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - // 详细信息 - if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() || - zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) { - - Spacer(modifier = Modifier.height(12.dp)) - HorizontalDivider( - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), - thickness = 0.5.dp - ) - Spacer(modifier = Modifier.height(8.dp)) - - // 版本信息 - if (zipFile.version.isNotEmpty()) { - InfoRow( - label = context.getString(R.string.version), - value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else "" - ) - } - - // 作者信息 - if (zipFile.author.isNotEmpty()) { - InfoRow( - label = context.getString(R.string.author), - value = zipFile.author - ) - } - - // 描述信息 (仅模块) - if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) { - InfoRow( - label = context.getString(R.string.description), - value = zipFile.description - ) - } - - // 支持设备 (仅内核) - if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) { - InfoRow( - label = context.getString(R.string.supported_devices), - value = zipFile.supported - ) - } - } - } - } -} - -@Composable -fun InfoRow(label: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 2.dp), - verticalAlignment = Alignment.Top - ) { - Text( - text = "$label:", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.widthIn(min = 60.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = value, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.weight(1f) - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt index 3c1b3580..150c25e8 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt @@ -25,4 +25,4 @@ fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) { LaunchedEffect(Unit) { requester.requestFocus() } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuValidCheck.kt similarity index 91% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuValidCheck.kt index eb3c5db0..0936f73d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuValidCheck.kt @@ -2,7 +2,6 @@ package com.sukisu.ultra.ui.component import androidx.compose.runtime.Composable import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp @Composable fun KsuIsValid( @@ -14,4 +13,4 @@ fun KsuIsValid( if (ksuVersion != null) { content() } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt deleted file mode 100644 index 03deff54..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.ui.theme.CardConfig - -private const val TAG = "SearchBar" - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchAppBar( - title: @Composable () -> Unit, - searchText: String, - onSearchTextChange: (String) -> Unit, - onClearClick: () -> Unit, - onBackClick: (() -> Unit)? = null, - onConfirm: (() -> Unit)? = null, - dropdownContent: @Composable (() -> Unit)? = null, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } - var onSearch by remember { mutableStateOf(false) } - - // 获取卡片颜色和透明度 - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - val cardAlpha = CardConfig.cardAlpha - - if (onSearch) { - LaunchedEffect(Unit) { focusRequester.requestFocus() } - } - DisposableEffect(Unit) { - onDispose { - keyboardController?.hide() - } - } - - TopAppBar( - title = { - Box { - AnimatedVisibility( - modifier = Modifier.align(Alignment.CenterStart), - visible = !onSearch, - enter = fadeIn(), - exit = fadeOut(), - content = { title() } - ) - - AnimatedVisibility( - visible = onSearch, - enter = fadeIn(), - exit = fadeOut() - ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp, bottom = 2.dp, end = if (onBackClick != null) 0.dp else 14.dp) - .focusRequester(focusRequester) - .onFocusChanged { focusState -> - if (focusState.isFocused) onSearch = true - Log.d(TAG, "onFocusChanged: $focusState") - }, - value = searchText, - onValueChange = onSearchTextChange, - trailingIcon = { - IconButton( - onClick = { - onSearch = false - keyboardController?.hide() - onClearClick() - }, - content = { Icon(Icons.Filled.Close, null) } - ) - }, - maxLines = 1, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - onConfirm?.invoke() - }) - ) - } - } - }, - navigationIcon = { - if (onBackClick != null) { - IconButton( - onClick = onBackClick, - content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) } - ) - } - }, - actions = { - AnimatedVisibility( - visible = !onSearch - ) { - IconButton( - onClick = { onSearch = true }, - content = { Icon(Icons.Filled.Search, null) } - ) - } - - if (dropdownContent != null) { - dropdownContent() - } - - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ) - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -private fun SearchAppBarPreview() { - var searchText by remember { mutableStateOf("") } - SearchAppBar( - title = { Text("Search text") }, - searchText = searchText, - onSearchTextChange = { searchText = it }, - onClearClick = { searchText = "" } - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SendLogDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SendLogDialog.kt new file mode 100644 index 00000000..00d1a1d7 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SendLogDialog.kt @@ -0,0 +1,154 @@ +package com.sukisu.ultra.ui.component + +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Save +import androidx.compose.material.icons.rounded.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import com.sukisu.ultra.BuildConfig +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.util.getBugreportFile +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +fun SendLogDialog( + showDialog: MutableState, + loadingDialog: LoadingDialogHandle, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val exportBugreportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/gzip") + ) { uri: Uri? -> + if (uri == null) return@rememberLauncherForActivityResult + scope.launch(Dispatchers.IO) { + loadingDialog.show() + context.contentResolver.openOutputStream(uri)?.use { output -> + getBugreportFile(context).inputStream().use { + it.copyTo(output) + } + } + loadingDialog.hide() + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.log_saved), Toast.LENGTH_SHORT).show() + } + } + } + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.send_log), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = colorScheme.onSurface + ) + SuperArrow( + title = stringResource(id = R.string.save_log), + leftAction = { + Icon( + Icons.Rounded.Save, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onSurface + ) + }, + onClick = { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") + val current = LocalDateTime.now().format(formatter) + exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") + showDialog.value = false + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + SuperArrow( + title = stringResource(id = R.string.send_log), + leftAction = { + Icon( + Icons.Rounded.Share, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onSurface + ) + }, + onClick = { + scope.launch { + showDialog.value = false + 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) + ) + ) + } + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt deleted file mode 100644 index 6ccf4285..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.selection.toggleable -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.semantics.Role -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.text.TextRow -import com.sukisu.ultra.ui.theme.CardConfig - -@Composable -fun SwitchItem( - icon: ImageVector? = null, - title: String, - summary: String? = null, - checked: Boolean, - enabled: Boolean = true, - beta: Boolean = false, - onCheckedChange: (Boolean) -> Unit -) { - val interactionSource = remember { MutableInteractionSource() } - val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) } - - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - ListItem( - modifier = Modifier - .toggleable( - value = checked, - interactionSource = interactionSource, - role = Role.Switch, - enabled = enabled, - indication = LocalIndication.current, - onValueChange = onCheckedChange - ), - headlineContent = { - TextRow( - leadingContent = if (beta) { - { - LabelItem( - modifier = Modifier.then(stateAlpha), - text = "Beta" - ) - } - } else null - ) { - Text( - modifier = Modifier.then(stateAlpha), - text = title, - ) - } - }, - leadingContent = icon?.let { - { - Icon( - modifier = Modifier.then(stateAlpha), - imageVector = icon, - contentDescription = title - ) - } - }, - trailingContent = { - Switch( - checked = checked, - enabled = enabled, - onCheckedChange = onCheckedChange, - interactionSource = interactionSource - ) - }, - supportingContent = { - if (summary != null) { - Text( - modifier = Modifier.then(stateAlpha), - text = summary - ) - } - } - ) - } -} - -@Composable -fun RadioItem( - title: String, - selected: Boolean, - onClick: () -> Unit, -) { - ListItem( - headlineContent = { - Text(title) - }, - leadingContent = { - RadioButton(selected = selected, onClick = onClick) - } - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt index 9103e555..a5fbd4b3 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt @@ -1,250 +1,299 @@ package com.sukisu.ultra.ui.component +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.BlendModeColorFilter import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.BasicComponentColors +import top.yukonga.miuix.kmp.basic.BasicComponentDefaults +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.ArrowUpDownIntegrated +import top.yukonga.miuix.kmp.icon.icons.basic.Check +import top.yukonga.miuix.kmp.theme.MiuixTheme -@OptIn(ExperimentalMaterial3Api::class) +/** + * A dropdown with a title and a summary. + * + * @param items The options of the [SuperDropdown]. + * @param selectedIndex The index of the selected option. + * @param title The title of the [SuperDropdown]. + * @param titleColor The color of the title. + * @param summary The summary of the [SuperDropdown]. + * @param summaryColor The color of the summary. + * @param dropdownColors The [DropdownColors] of the [SuperDropdown]. + * @param insideMargin The margin inside the [SuperDropdown]. + * @param maxHeight The maximum height of the [ListPopup]. + * @param enabled Whether the [SuperDropdown] is enabled. + * @param showValue Whether to show the selected value of the [SuperDropdown]. + * @param onClick The callback when the [SuperDropdown] is clicked. + * @param onSelectedIndexChange The callback when the selected index of the [SuperDropdown] is changed. + */ @Composable fun SuperDropdown( items: List, selectedIndex: Int, title: String, + titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), summary: String? = null, - icon: ImageVector? = null, + summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), + dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), + leftAction: (@Composable (() -> Unit))? = null, + insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, + maxHeight: Dp? = null, enabled: Boolean = true, showValue: Boolean = true, - maxHeight: Dp? = 400.dp, - colors: SuperDropdownColors = SuperDropdownDefaults.colors(), - leftAction: (@Composable () -> Unit)? = null, - onSelectedIndexChange: (Int) -> Unit + onClick: (() -> Unit)? = null, + onSelectedIndexChange: ((Int) -> Unit)?, ) { - var showDialog by remember { mutableStateOf(false) } - val selectedItemText = items.getOrNull(selectedIndex) ?: "" + val interactionSource = remember { MutableInteractionSource() } + val isDropdownExpanded = remember { mutableStateOf(false) } + val hapticFeedback = LocalHapticFeedback.current + val itemsNotEmpty = items.isNotEmpty() val actualEnabled = enabled && itemsNotEmpty - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = actualEnabled) { showDialog = true } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.Top - ) { - if (leftAction != null) { - leftAction() - } else if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - } - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor - ) - - if (summary != null) { - Spacer(modifier = Modifier.height(3.dp)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor - ) - } - - if (showValue && itemsNotEmpty) { - Spacer(modifier = Modifier.height(3.dp)) - Text( - text = selectedItemText, - style = MaterialTheme.typography.bodyMedium, - color = if (actualEnabled) colors.valueColor else colors.disabledValueColor, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - } - - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor, - modifier = Modifier.size(24.dp) - ) + val actionColor = if (actualEnabled) { + MiuixTheme.colorScheme.onSurfaceVariantActions + } else { + MiuixTheme.colorScheme.disabledOnSecondaryVariant } - if (showDialog && itemsNotEmpty) { - AlertDialog( - onDismissRequest = { showDialog = false }, - title = { - Text( - text = title, - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - val dialogMaxHeight = maxHeight ?: 400.dp - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = dialogMaxHeight), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(items.size) { index -> - DropdownItem( - text = items[index], - isSelected = selectedIndex == index, - colors = colors, - onClick = { - onSelectedIndexChange(index) - showDialog = false - } - ) - } + val handleClick: () -> Unit = { + if (actualEnabled) { + onClick?.invoke() + isDropdownExpanded.value = !isDropdownExpanded.value + if (isDropdownExpanded.value) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) + } + } + } + + BasicComponent( + interactionSource = interactionSource, + insideMargin = insideMargin, + title = title, + titleColor = titleColor, + summary = summary, + summaryColor = summaryColor, + leftAction = if (itemsNotEmpty) { + { + SuperDropdownPopup( + items = items, + selectedIndex = selectedIndex, + isDropdownExpanded = isDropdownExpanded, + maxHeight = maxHeight, + dropdownColors = dropdownColors, + hapticFeedback = hapticFeedback, + onSelectedIndexChange = onSelectedIndexChange + ) + leftAction?.invoke() + } + } else null, + rightActions = { + SuperDropdownRightActions( + showValue = showValue, + itemsNotEmpty = itemsNotEmpty, + items = items, + selectedIndex = selectedIndex, + actionColor = actionColor + ) + }, + onClick = handleClick, + holdDownState = isDropdownExpanded.value, + enabled = actualEnabled + ) +} + +@Composable +private fun SuperDropdownPopup( + items: List, + selectedIndex: Int, + isDropdownExpanded: MutableState, + maxHeight: Dp?, + dropdownColors: DropdownColors, + hapticFeedback: HapticFeedback, + onSelectedIndexChange: ((Int) -> Unit)? +) { + val onSelectState = rememberUpdatedState(onSelectedIndexChange) + ListPopup( + show = isDropdownExpanded, + alignment = PopupPositionProvider.Align.Right, + onDismissRequest = { + isDropdownExpanded.value = false + }, + maxHeight = maxHeight + ) { + ListPopupColumn { + items.forEachIndexed { index, string -> + key(index) { + DropdownImpl( + text = string, + optionSize = items.size, + isSelected = selectedIndex == index, + dropdownColors = dropdownColors, + onSelectedIndexChange = { selectedIdx -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + onSelectState.value?.invoke(selectedIdx) + isDropdownExpanded.value = false + }, + index = index + ) } - }, - confirmButton = { - TextButton(onClick = { showDialog = false }) { - Text(text = stringResource(id = android.R.string.cancel)) - } - }, - containerColor = colors.dialogBackgroundColor, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) + } + } } } @Composable -private fun DropdownItem( - text: String, - isSelected: Boolean, - colors: SuperDropdownColors, - onClick: () -> Unit +private fun RowScope.SuperDropdownRightActions( + showValue: Boolean, + itemsNotEmpty: Boolean, + items: List, + selectedIndex: Int, + actionColor: Color ) { - val backgroundColor = if (isSelected) { - colors.selectedBackgroundColor + if (showValue && itemsNotEmpty) { + Text( + modifier = Modifier.widthIn(max = 130.dp), + text = items[selectedIndex], + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = actionColor, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } + + Image( + modifier = Modifier + .padding(start = 8.dp) + .size(10.dp, 16.dp) + .align(Alignment.CenterVertically), + imageVector = MiuixIcons.Basic.ArrowUpDownIntegrated, + colorFilter = ColorFilter.tint(actionColor), + contentDescription = null + ) +} + +/** + * The implementation of the dropdown. + * + * @param text The text of the current option. + * @param optionSize The size of the options. + * @param isSelected Whether the option is selected. + * @param index The index of the current option in the options. + * @param onSelectedIndexChange The callback when the index is selected. + */ +@Composable +fun DropdownImpl( + text: String, + optionSize: Int, + isSelected: Boolean, + index: Int, + dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), + onSelectedIndexChange: (Int) -> Unit +) { + val additionalTopPadding = if (index == 0) 20.dp else 12.dp + val additionalBottomPadding = if (index == optionSize - 1) 20.dp else 12.dp + + val (textColor, backgroundColor) = if (isSelected) { + dropdownColors.selectedContentColor to dropdownColors.selectedContainerColor + } else { + dropdownColors.contentColor to dropdownColors.containerColor + } + + val checkColor = if (isSelected) { + dropdownColors.selectedContentColor } else { Color.Transparent } - val contentColor = if (isSelected) { - colors.selectedContentColor - } else { - colors.contentColor - } - Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) + .clickable { onSelectedIndexChange(index) } .background(backgroundColor) - .clickable(onClick = onClick) - .padding(vertical = 12.dp, horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 20.dp) + .padding( + top = additionalTopPadding, + bottom = additionalBottomPadding + ) ) { - RadioButton( - selected = isSelected, - onClick = null, - colors = RadioButtonDefaults.colors( - selectedColor = colors.selectedContentColor, - unselectedColor = colors.contentColor - ) - ) - - Spacer(modifier = Modifier.width(12.dp)) - Text( + modifier = Modifier.widthIn(max = 200.dp), text = text, - style = MaterialTheme.typography.bodyLarge, - color = contentColor, - modifier = Modifier.weight(1f) + fontSize = MiuixTheme.textStyles.body1.fontSize, + fontWeight = FontWeight.Medium, + color = textColor, + ) + + Image( + modifier = Modifier + .padding(start = 12.dp) + .size(20.dp), + imageVector = MiuixIcons.Basic.Check, + colorFilter = BlendModeColorFilter(checkColor, BlendMode.SrcIn), + contentDescription = null, ) - - if (isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - tint = colors.selectedContentColor, - modifier = Modifier.size(20.dp) - ) - } } } @Immutable -data class SuperDropdownColors( - val titleColor: Color, - val summaryColor: Color, - val valueColor: Color, - val iconColor: Color, - val arrowColor: Color, - val disabledTitleColor: Color, - val disabledSummaryColor: Color, - val disabledValueColor: Color, - val disabledIconColor: Color, - val disabledArrowColor: Color, - val dialogBackgroundColor: Color, +class DropdownColors( val contentColor: Color, + val containerColor: Color, val selectedContentColor: Color, - val selectedBackgroundColor: Color + val selectedContainerColor: Color ) -object SuperDropdownDefaults { +object DropdownDefaults { + @Composable - fun colors( - titleColor: Color = MaterialTheme.colorScheme.onSurface, - summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, - valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, - iconColor: Color = MaterialTheme.colorScheme.primary, - arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, - disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), - disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), - disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), - disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), - dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor: Color = MaterialTheme.colorScheme.onSurface, - selectedContentColor: Color = MaterialTheme.colorScheme.primary, - selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - ): SuperDropdownColors { - return SuperDropdownColors( - titleColor = titleColor, - summaryColor = summaryColor, - valueColor = valueColor, - iconColor = iconColor, - arrowColor = arrowColor, - disabledTitleColor = disabledTitleColor, - disabledSummaryColor = disabledSummaryColor, - disabledValueColor = disabledValueColor, - disabledIconColor = disabledIconColor, - disabledArrowColor = disabledArrowColor, - dialogBackgroundColor = dialogBackgroundColor, + fun dropdownColors( + contentColor: Color = MiuixTheme.colorScheme.onSurface, + containerColor: Color = MiuixTheme.colorScheme.surface, + selectedContentColor: Color = MiuixTheme.colorScheme.onTertiaryContainer, + selectedContainerColor: Color = MiuixTheme.colorScheme.surface + ): DropdownColors { + return DropdownColors( contentColor = contentColor, + containerColor = containerColor, selectedContentColor = selectedContentColor, - selectedBackgroundColor = selectedBackgroundColor + selectedContainerColor = selectedContainerColor ) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperEditArrow.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperEditArrow.kt new file mode 100644 index 00000000..2b8ffd0d --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperEditArrow.kt @@ -0,0 +1,132 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.filter.FilterNumber +import top.yukonga.miuix.kmp.basic.BasicComponentColors +import top.yukonga.miuix.kmp.basic.BasicComponentDefaults +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.extra.RightActionColors +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperArrowDefaults +import top.yukonga.miuix.kmp.extra.SuperDialog + +@Composable +fun SuperEditArrow( + title: String, + titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), + defaultValue: Int = -1, + summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), + leftAction: @Composable (() -> Unit)? = null, + rightActionColor: RightActionColors = SuperArrowDefaults.rightActionColors(), + modifier: Modifier = Modifier, + insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, + enabled: Boolean = true, + onValueChange: ((Int) -> Unit)? = null +) { + val showDialog = remember { mutableStateOf(false) } + val dialogTextFieldValue = remember { mutableIntStateOf(defaultValue) } + + SuperArrow( + title = title, + titleColor = titleColor, + summary = dialogTextFieldValue.intValue.toString(), + summaryColor = summaryColor, + leftAction = leftAction, + rightActionColor = rightActionColor, + modifier = modifier, + insideMargin = insideMargin, + onClick = { + showDialog.value = true + }, + holdDownState = showDialog.value, + enabled = enabled + ) + + EditDialog( + title, + showDialog, + dialogTextFieldValue = dialogTextFieldValue.intValue, + ) { + dialogTextFieldValue.intValue = it + onValueChange?.invoke(dialogTextFieldValue.intValue) + } + +} + +@Composable +private fun EditDialog( + title: String, + showDialog: MutableState, + dialogTextFieldValue: Int, + onValueChange: (Int) -> Unit, +) { + val inputTextFieldValue = remember { mutableIntStateOf(dialogTextFieldValue) } + val filter = remember(key1 = inputTextFieldValue.intValue) { FilterNumber(dialogTextFieldValue) } + + SuperDialog( + title = title, + show = showDialog, + onDismissRequest = { + showDialog.value = false + filter.setInputValue(dialogTextFieldValue.toString()) + } + ) { + TextField( + modifier = Modifier.padding(bottom = 16.dp), + value = filter.getInputValue(), + maxLines = 1, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + onValueChange = filter.onValueChange() + ) + Row( + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + text = stringResource(android.R.string.cancel), + onClick = { + showDialog.value = false + filter.setInputValue(dialogTextFieldValue.toString()) + }, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(20.dp)) + TextButton( + text = stringResource(R.string.confirm), + onClick = { + showDialog.value = false + with(filter.getInputValue().text) { + if (isEmpty()) { + onValueChange(0) + filter.setInputValue("0") + } else { + onValueChange(this@with.toInt()) + } + + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperSearchBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperSearchBar.kt new file mode 100644 index 00000000..998a1400 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperSearchBar.kt @@ -0,0 +1,408 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +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.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.hazeEffect +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.InputField +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.Search +import top.yukonga.miuix.kmp.icon.icons.basic.SearchCleanup +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.BackHandler +import top.yukonga.miuix.kmp.utils.overScrollVertical + +// Search Status Class +@Stable +class SearchStatus(val label: String) { + var searchText by mutableStateOf("") + var current by mutableStateOf(Status.COLLAPSED) + + var offsetY by mutableStateOf(0.dp) + var resultStatus by mutableStateOf(ResultStatus.DEFAULT) + + fun isExpand() = current == Status.EXPANDED + fun isCollapsed() = current == Status.COLLAPSED + fun shouldExpand() = current == Status.EXPANDED || current == Status.EXPANDING + fun shouldCollapsed() = current == Status.COLLAPSED || current == Status.COLLAPSING + fun isAnimatingExpand() = current == Status.EXPANDING + + // 动画完成回调 + fun onAnimationComplete() { + current = when (current) { + Status.EXPANDING -> Status.EXPANDED + Status.COLLAPSING -> { + searchText = "" + Status.COLLAPSED + } + + else -> current + } + } + + @Composable + fun TopAppBarAnim( + modifier: Modifier = Modifier, + visible: Boolean = shouldCollapsed(), + hazeState: HazeState? = null, + hazeStyle: HazeStyle? = null, + content: @Composable () -> Unit + ) { + val topAppBarAlpha = animateFloatAsState( + if (visible) 1f else 0f, + animationSpec = tween(if (visible) 550 else 0, easing = FastOutSlowInEasing), + ) + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .then( + if (hazeState != null && hazeStyle != null) { + Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + } + } else { + Modifier.background(colorScheme.background) + } + ) + ) + Box( + modifier = Modifier + .alpha(topAppBarAlpha.value) + ) { content() } + } + } + + enum class Status { EXPANDED, EXPANDING, COLLAPSED, COLLAPSING } + enum class ResultStatus { DEFAULT, EMPTY, LOAD, SHOW } +} + +// Search Box Composable +@Composable +fun SearchStatus.SearchBox( + collapseBar: @Composable (SearchStatus, Dp, PaddingValues) -> Unit = { searchStatus, topPadding, innerPadding -> + SearchBarFake(searchStatus.label, topPadding, innerPadding) + }, + searchBarTopPadding: Dp = 12.dp, + contentPadding: PaddingValues = PaddingValues(0.dp), + hazeState: HazeState, + hazeStyle: HazeStyle, + content: @Composable (MutableState) -> Unit +) { + val searchStatus = this + val density = LocalDensity.current + + animateFloatAsState(if (searchStatus.shouldCollapsed()) 1f else 0f) + + val offsetY = remember { mutableIntStateOf(0) } + val boxHeight = remember { mutableStateOf(0.dp) } + + Box( + modifier = Modifier + .fillMaxWidth() + .zIndex(10f) + .alpha(if (searchStatus.isCollapsed()) 1f else 0f) + .offset(y = contentPadding.calculateTopPadding()) + .onGloballyPositioned { + it.positionInWindow().y.apply { + offsetY.intValue = (this@apply * 0.9).toInt() + with(density) { + searchStatus.offsetY = this@apply.toDp() + boxHeight.value = it.size.height.toDp() + } + } + } + .pointerInput(Unit) { + detectTapGestures { searchStatus.current = SearchStatus.Status.EXPANDING } + } + .hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + } + ) { + collapseBar(searchStatus, searchBarTopPadding, contentPadding) + } + Box { + AnimatedVisibility( + visible = searchStatus.shouldCollapsed(), + enter = fadeIn(tween(300, easing = LinearOutSlowInEasing)) + slideInVertically( + tween( + 300, + easing = LinearOutSlowInEasing + ) + ) { -offsetY.intValue }, + exit = fadeOut(tween(300, easing = LinearOutSlowInEasing)) + slideOutVertically( + tween( + 300, + easing = LinearOutSlowInEasing + ) + ) { -offsetY.intValue } + ) { + content(boxHeight) + } + } +} + +// Search Pager Composable +@Composable +fun SearchStatus.SearchPager( + defaultResult: @Composable () -> Unit, + expandBar: @Composable (SearchStatus, Dp) -> Unit = { searchStatus, padding -> + SearchBar(searchStatus, padding) + }, + searchBarTopPadding: Dp = 12.dp, + result: LazyListScope.() -> Unit +) { + val searchStatus = this + val systemBarsPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() + val topPadding by animateDpAsState( + if (searchStatus.shouldExpand()) systemBarsPadding + 5.dp else searchStatus.offsetY, + animationSpec = tween(300, easing = LinearOutSlowInEasing) + ) { + searchStatus.onAnimationComplete() + } + val backgroundAlpha by animateFloatAsState( + if (searchStatus.shouldExpand()) 1f else 0f, + animationSpec = tween(200, easing = FastOutSlowInEasing) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .zIndex(5f) + .background(colorScheme.background.copy(alpha = backgroundAlpha)) + .semantics { onClick { false } } + .then( + if (!searchStatus.isCollapsed()) Modifier.pointerInput(Unit) { } else Modifier + ) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(top = topPadding) + .then( + if (!searchStatus.isCollapsed()) Modifier.background(colorScheme.background) + else Modifier + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + if (!searchStatus.isCollapsed()) { + Box( + modifier = Modifier + .weight(1f) + .background(colorScheme.background) + ) { + expandBar(searchStatus, searchBarTopPadding) + } + } + AnimatedVisibility( + visible = searchStatus.isExpand() || searchStatus.isAnimatingExpand(), + enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }), + exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }) + ) { + Text( + text = stringResource(android.R.string.cancel), + fontWeight = FontWeight.Bold, + color = colorScheme.primary, + modifier = Modifier + .padding(start = 4.dp, end = 16.dp, top = searchBarTopPadding) + .clickable( + interactionSource = null, + enabled = searchStatus.isExpand(), + indication = null + ) { searchStatus.current = SearchStatus.Status.COLLAPSING } + ) + BackHandler(enabled = true) { + searchStatus.current = SearchStatus.Status.COLLAPSING + } + } + } + AnimatedVisibility( + visible = searchStatus.isExpand(), + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + enter = fadeIn(), + exit = fadeOut() + ) { + when (searchStatus.resultStatus) { + SearchStatus.ResultStatus.DEFAULT -> defaultResult() + SearchStatus.ResultStatus.EMPTY -> {} + SearchStatus.ResultStatus.LOAD -> {} + SearchStatus.ResultStatus.SHOW -> LazyColumn( + Modifier + .fillMaxSize() + .overScrollVertical(), + ) { + result() + } + } + } + } +} + +@Composable +fun SearchBar( + searchStatus: SearchStatus, + searchBarTopPadding: Dp = 12.dp, +) { + val focusRequester = remember { FocusRequester() } + var expanded by rememberSaveable { mutableStateOf(false) } + + InputField( + query = searchStatus.searchText, + onQueryChange = { searchStatus.searchText = it }, + label = "", + leadingIcon = { + Icon( + imageVector = MiuixIcons.Basic.Search, + contentDescription = "back", + modifier = Modifier + .size(44.dp) + .padding(start = 16.dp, end = 8.dp), + tint = colorScheme.onSurfaceContainerHigh, + ) + }, + trailingIcon = { + AnimatedVisibility( + searchStatus.searchText.isNotEmpty(), + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + ) { + Icon( + imageVector = MiuixIcons.Basic.SearchCleanup, + tint = colorScheme.onSurface, + contentDescription = "Clean", + modifier = Modifier + .size(44.dp) + .padding(start = 8.dp, end = 16.dp) + .clickable( + interactionSource = null, + indication = null + ) { + searchStatus.searchText = "" + }, + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(top = searchBarTopPadding, bottom = 6.dp) + .focusRequester(focusRequester), + onSearch = { it }, + expanded = searchStatus.shouldExpand(), + onExpandedChange = { + searchStatus.current = if (it) SearchStatus.Status.EXPANDED else SearchStatus.Status.COLLAPSED + } + ) + LaunchedEffect(Unit) { + if (!expanded && searchStatus.shouldExpand()) { + focusRequester.requestFocus() + expanded = true + } + } +} + +@Composable +fun SearchBarFake( + label: String, + searchBarTopPadding: Dp = 12.dp, + innerPadding: PaddingValues = PaddingValues(0.dp) +) { + val layoutDirection = LocalLayoutDirection.current + InputField( + query = "", + onQueryChange = { }, + label = label, + leadingIcon = { + Icon( + imageVector = MiuixIcons.Basic.Search, + contentDescription = "Clean", + modifier = Modifier + .size(44.dp) + .padding(start = 16.dp, end = 8.dp), + tint = colorScheme.onSurfaceContainerHigh, + ) + }, + modifier = Modifier + .padding(horizontal = 12.dp) + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ) + .padding(top = searchBarTopPadding, bottom = 6.dp), + onSearch = { }, + enabled = false, + expanded = false, + onExpandedChange = { } + ) +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/UninstallDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/UninstallDialog.kt new file mode 100644 index 00000000..b42acad8 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/UninstallDialog.kt @@ -0,0 +1,140 @@ +package com.sukisu.ultra.ui.component + +import android.widget.Toast +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.screen.FlashIt +import com.sukisu.ultra.ui.screen.UninstallType +import com.sukisu.ultra.ui.screen.UninstallType.NONE +import com.sukisu.ultra.ui.screen.UninstallType.PERMANENT +import com.sukisu.ultra.ui.screen.UninstallType.RESTORE_STOCK_IMAGE +import com.sukisu.ultra.ui.screen.UninstallType.TEMPORARY +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun UninstallDialog( + showDialog: MutableState, + navigator: DestinationsNavigator, +) { + val context = LocalContext.current + val options = listOf( + // TEMPORARY, + PERMANENT, + RESTORE_STOCK_IMAGE + ) + val showTodo = { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() + } + val showConfirmDialog = remember(showDialog.value) { mutableStateOf(false) } + val runType = remember(showDialog.value) { mutableStateOf(null) } + + val run = { type: UninstallType -> + when (type) { + PERMANENT -> navigator.navigate(FlashScreenDestination(FlashIt.FlashUninstall)) { + popUpTo(FlashScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + + RESTORE_STOCK_IMAGE -> navigator.navigate(FlashScreenDestination(FlashIt.FlashRestore)) { + popUpTo(FlashScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + + TEMPORARY -> showTodo() + NONE -> Unit + } + } + + SuperDialog( + show = showDialog, + insideMargin = DpSize(0.dp, 0.dp), + onDismissRequest = { + showDialog.value = false + }, + content = { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 12.dp), + text = stringResource(R.string.uninstall), + fontSize = MiuixTheme.textStyles.title4.fontSize, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MiuixTheme.colorScheme.onSurface + ) + options.forEachIndexed { index, type -> + SuperArrow( + onClick = { + showConfirmDialog.value = true + runType.value = type + }, + title = stringResource(type.title), + leftAction = { + Icon( + imageVector = type.icon, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = MiuixTheme.colorScheme.onSurface + ) + }, + insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) + ) + } + TextButton( + text = stringResource(id = android.R.string.cancel), + onClick = { + showDialog.value = false + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp) + .padding(horizontal = 24.dp) + ) + } + ) + val confirmDialog = rememberConfirmDialog( + onConfirm = { + showConfirmDialog.value = false + showDialog.value = false + runType.value?.let { type -> + run(type) + } + }, + onDismiss = { + showConfirmDialog.value = false + } + ) + val dialogTitle = runType.value?.let { type -> + options.find { it == type }?.let { stringResource(it.title) } + } ?: "" + val dialogContent = runType.value?.let { type -> + options.find { it == type }?.let { stringResource(it.message) } + } + if (showConfirmDialog.value) { + confirmDialog.showConfirm(title = dialogTitle, content = dialogContent) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt deleted file mode 100644 index e37cd81a..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt +++ /dev/null @@ -1,257 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.R - -data class FabMenuItem( - val icon: ImageVector, - val labelRes: Int, - val color: Color = Color.Unspecified, - val onClick: () -> Unit -) - -object FabAnimationConfig { - const val ANIMATION_DURATION = 300 - const val STAGGER_DELAY = 50 - val BUTTON_SPACING = 72.dp - val BUTTON_SIZE = 56.dp - val SMALL_BUTTON_SIZE = 48.dp -} - -@Composable -fun VerticalExpandableFab( - menuItems: List, - modifier: Modifier = Modifier, - buttonSize: Dp = FabAnimationConfig.BUTTON_SIZE, - smallButtonSize: Dp = FabAnimationConfig.SMALL_BUTTON_SIZE, - buttonSpacing: Dp = FabAnimationConfig.BUTTON_SPACING, - animationDurationMs: Int = FabAnimationConfig.ANIMATION_DURATION, - staggerDelayMs: Int = FabAnimationConfig.STAGGER_DELAY, - mainButtonIcon: ImageVector = Icons.Filled.Add, - mainButtonExpandedIcon: ImageVector = Icons.Filled.Close, - onMainButtonClick: (() -> Unit)? = null, -) { - var isExpanded by remember { mutableStateOf(false) } - - val rotationAngle by animateFloatAsState( - targetValue = if (isExpanded) 45f else 0f, - animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), - label = "mainButtonRotation" - ) - - val mainButtonScale by animateFloatAsState( - targetValue = if (isExpanded) 1.1f else 1f, - animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), - label = "mainButtonScale" - ) - - Box( - modifier = modifier.wrapContentSize(), - contentAlignment = Alignment.BottomEnd - ) { - menuItems.forEachIndexed { index, menuItem -> - val animatedOffsetY by animateFloatAsState( - targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f, - animationSpec = tween( - durationMillis = animationDurationMs, - delayMillis = if (isExpanded) { - index * staggerDelayMs - } else { - (menuItems.size - index - 1) * staggerDelayMs - }, - easing = FastOutSlowInEasing - ), - label = "fabOffset$index" - ) - - val animatedScale by animateFloatAsState( - targetValue = if (isExpanded) 1f else 0f, - animationSpec = tween( - durationMillis = animationDurationMs, - delayMillis = if (isExpanded) { - index * staggerDelayMs + 100 - } else { - (menuItems.size - index - 1) * staggerDelayMs - }, - easing = FastOutSlowInEasing - ), - label = "fabScale$index" - ) - - val animatedAlpha by animateFloatAsState( - targetValue = if (isExpanded) 1f else 0f, - animationSpec = tween( - durationMillis = animationDurationMs, - delayMillis = if (isExpanded) { - index * staggerDelayMs + 150 - } else { - (menuItems.size - index - 1) * staggerDelayMs - }, - easing = FastOutSlowInEasing - ), - label = "fabAlpha$index" - ) - - Row( - modifier = Modifier - .offset(y = animatedOffsetY.dp) - .scale(animatedScale) - .alpha(animatedAlpha), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - AnimatedVisibility( - visible = isExpanded && animatedScale > 0.5f, - enter = slideInHorizontally( - initialOffsetX = { it / 2 }, - animationSpec = tween(200) - ) + fadeIn(animationSpec = tween(200)), - exit = slideOutHorizontally( - targetOffsetX = { it / 2 }, - animationSpec = tween(150) - ) + fadeOut(animationSpec = tween(150)) - ) { - Surface( - modifier = Modifier.padding(end = 16.dp), - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.inverseSurface, - tonalElevation = 6.dp - ) { - Text( - text = stringResource(menuItem.labelRes), - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.inverseOnSurface - ) - } - } - - SmallFloatingActionButton( - onClick = { - menuItem.onClick() - isExpanded = false - }, - modifier = Modifier.size(smallButtonSize), - containerColor = if (menuItem.color != Color.Unspecified) { - menuItem.color - } else { - MaterialTheme.colorScheme.secondary - }, - contentColor = if (menuItem.color != Color.Unspecified) { - if (menuItem.color == Color.Gray) Color.White - else MaterialTheme.colorScheme.onSecondary - } else { - MaterialTheme.colorScheme.onSecondary - }, - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 4.dp, - pressedElevation = 6.dp - ) - ) { - Icon( - imageVector = menuItem.icon, - contentDescription = stringResource(menuItem.labelRes), - modifier = Modifier.size(20.dp) - ) - } - } - } - - FloatingActionButton( - onClick = { - onMainButtonClick?.invoke() - isExpanded = !isExpanded - }, - modifier = Modifier.size(buttonSize).scale(mainButtonScale), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 6.dp, - pressedElevation = 8.dp, - hoveredElevation = 8.dp - ) - ) { - Icon( - imageVector = if (isExpanded) mainButtonExpandedIcon else mainButtonIcon, - contentDescription = stringResource( - if (isExpanded) R.string.collapse_menu else R.string.expand_menu - ), - modifier = Modifier - .size(24.dp) - .rotate(if (mainButtonIcon == Icons.Filled.Add) rotationAngle else 0f) - ) - } - } -} - -object FabMenuPresets { - fun getScrollMenuItems( - onScrollToTop: () -> Unit, - onScrollToBottom: () -> Unit - ) = listOf( - FabMenuItem( - icon = Icons.Filled.KeyboardArrowDown, - labelRes = R.string.scroll_to_bottom, - onClick = onScrollToBottom - ), - FabMenuItem( - icon = Icons.Filled.KeyboardArrowUp, - labelRes = R.string.scroll_to_top, - onClick = onScrollToTop - ) - ) - - @Composable - fun getBatchActionMenuItems( - onCancel: () -> Unit, - onDeny: () -> Unit, - onAllow: () -> Unit, - onUnmountModules: () -> Unit, - onDisableUnmount: () -> Unit - ) = listOf( - FabMenuItem( - icon = Icons.Filled.Close, - labelRes = R.string.cancel, - color = Color.Gray, - onClick = onCancel - ), - FabMenuItem( - icon = Icons.Filled.Block, - labelRes = R.string.deny_authorization, - color = MaterialTheme.colorScheme.error, - onClick = onDeny - ), - FabMenuItem( - icon = Icons.Filled.Check, - labelRes = R.string.grant_authorization, - color = MaterialTheme.colorScheme.primary, - onClick = onAllow - ), - FabMenuItem( - icon = Icons.Filled.FolderOff, - labelRes = R.string.unmount_modules, - onClick = onUnmountModules - ), - FabMenuItem( - icon = Icons.Filled.Folder, - labelRes = R.string.disable_unmount, - onClick = onDisableUnmount - ) - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/BaseFieldFilter.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/BaseFieldFilter.kt new file mode 100644 index 00000000..1bb03a9b --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/BaseFieldFilter.kt @@ -0,0 +1,51 @@ +package com.sukisu.ultra.ui.component.filter + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue + +open class BaseFieldFilter() { + private var inputValue = mutableStateOf(TextFieldValue()) + + constructor(value: String) : this() { + inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) + } + + protected open fun onFilter(inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue): TextFieldValue { + return TextFieldValue() + } + + protected open fun computePos(): Int { + // TODO + return 0 + } + + protected fun getNewTextRange( + lastTextFiled: TextFieldValue, + inputTextFieldValue: TextFieldValue + ): TextRange? { + return null + } + + protected fun getNewText( + lastTextFiled: TextFieldValue, + inputTextFieldValue: TextFieldValue + ): TextRange? { + + return null + } + + fun setInputValue(value: String) { + inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) + } + + fun getInputValue(): TextFieldValue { + return inputValue.value + } + + fun onValueChange(): (TextFieldValue) -> Unit { + return { + inputValue.value = onFilter(it, inputValue.value) + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/FilterNumber.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/FilterNumber.kt new file mode 100644 index 00000000..c295444d --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/filter/FilterNumber.kt @@ -0,0 +1,82 @@ +package com.sukisu.ultra.ui.component.filter + +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue + +class FilterNumber( + private val value: Int, + private val minValue: Int = Int.MIN_VALUE, + private val maxValue: Int = Int.MAX_VALUE, +) : BaseFieldFilter(value.toString()) { + + override fun onFilter( + inputTextFieldValue: TextFieldValue, + lastTextFieldValue: TextFieldValue + ): TextFieldValue { + return filterInputNumber(inputTextFieldValue, lastTextFieldValue, minValue, maxValue) + } + + private fun filterInputNumber( + inputTextFieldValue: TextFieldValue, + lastInputTextFieldValue: TextFieldValue, + minValue: Int = Int.MIN_VALUE, + maxValue: Int = Int.MAX_VALUE, + ): TextFieldValue { + val inputString = inputTextFieldValue.text + lastInputTextFieldValue.text + + val newString = StringBuilder() + val supportNegative = minValue < 0 + var isNegative = false + + // 只允许负号在首位,并且只允许一个负号 + if (supportNegative && inputString.isNotEmpty() && inputString.first() == '-') { + isNegative = true + newString.append('-') + } + + for ((i, c) in inputString.withIndex()) { + if (i == 0 && isNegative) continue // 首字符已经处理 + when (c) { + in '0'..'9' -> { + newString.append(c) + // 检查是否超出范围 + val tempText = newString.toString() + // 只在不是单独 '-' 时做判断(因为 '-' toInt 会异常) + if (tempText != "-" && tempText.isNotEmpty()) { + try { + val tempValue = tempText.toInt() + if (tempValue > maxValue || tempValue < minValue) { + newString.deleteCharAt(newString.lastIndex) + } + } catch (e: NumberFormatException) { + // 超出int范围 + newString.deleteCharAt(newString.lastIndex) + } + } + } + // 忽略其他字符(包括点号) + } + } + + val textRange: TextRange + if (inputTextFieldValue.selection.collapsed) { // 表示的是光标范围 + if (inputTextFieldValue.selection.end != inputTextFieldValue.text.length) { // 光标没有指向末尾 + var newPosition = inputTextFieldValue.selection.end + (newString.length - inputString.length) + if (newPosition < 0) { + newPosition = inputTextFieldValue.selection.end + } + textRange = TextRange(newPosition) + } else { // 光标指向了末尾 + textRange = TextRange(newString.length) + } + } else { + textRange = TextRange(newString.length) + } + + return lastInputTextFieldValue.copy( + text = newString.toString(), + selection = textRange + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt index 5ba96952..42224dea 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt @@ -1,15 +1,18 @@ package com.sukisu.ultra.ui.component.profile import androidx.compose.foundation.layout.Column -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.sukisu.ultra.Natives import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.SwitchItem +import com.sukisu.ultra.ui.component.EditText +import top.yukonga.miuix.kmp.extra.SuperSwitch @Composable fun AppProfileConfig( @@ -21,13 +24,15 @@ fun AppProfileConfig( ) { Column(modifier = modifier) { if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, - value = profile.name, - onValueChange = { onProfileChange(profile.copy(name = it)) } + EditText( + title = stringResource(R.string.profile_name), + textValue = remember { mutableStateOf(profile.name) }, + onTextValueChange = { onProfileChange(profile.copy(name = it)) }, + enabled = enabled, ) } - SwitchItem( + + SuperSwitch( title = stringResource(R.string.profile_umount_modules), summary = stringResource(R.string.profile_umount_modules_summary), checked = if (enabled) { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt index 7593a491..7e76c936 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt @@ -1,34 +1,46 @@ package com.sukisu.ultra.ui.component.profile -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.core.text.isDigitsOnly -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.input.InputDialog -import com.maxkeppeler.sheets.input.models.* -import com.maxkeppeler.sheets.list.ListDialog -import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection import com.sukisu.ultra.Natives import com.sukisu.ultra.R import com.sukisu.ultra.profile.Capabilities import com.sukisu.ultra.profile.Groups -import com.sukisu.ultra.ui.component.rememberCustomDialog +import com.sukisu.ultra.ui.component.SuperEditArrow import com.sukisu.ultra.ui.util.isSepolicyValid +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.extra.CheckboxLocation +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperCheckbox +import top.yukonga.miuix.kmp.extra.SuperDialog +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RootProfileConfig( modifier: Modifier = Modifier, @@ -36,94 +48,49 @@ fun RootProfileConfig( profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { - Column(modifier = modifier) { + Column( + modifier = modifier + ) { if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, + TextField( + label = stringResource(R.string.profile_name), value = profile.name, onValueChange = { onProfileChange(profile.copy(name = it)) } ) } - /* - var expanded by remember { mutableStateOf(false) } - val currentNamespace = when (profile.namespace) { - Natives.Profile.Namespace.INHERITED.ordinal -> stringResource(R.string.profile_namespace_inherited) - Natives.Profile.Namespace.GLOBAL.ordinal -> stringResource(R.string.profile_namespace_global) - Natives.Profile.Namespace.INDIVIDUAL.ordinal -> stringResource(R.string.profile_namespace_individual) - else -> stringResource(R.string.profile_namespace_inherited) - } - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - label = { Text(stringResource(R.string.profile_namespace)) }, - value = currentNamespace, - onValueChange = {}, - trailingIcon = { - if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) - }, - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_inherited)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INHERITED.ordinal)) - expanded = false - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_global)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.GLOBAL.ordinal)) - expanded = false - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_individual)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal)) - expanded = false - }, - ) - } - } - }) - */ - - UidPanel(uid = profile.uid, label = "uid", onUidChange = { + SuperEditArrow( + title = "UID", + defaultValue = profile.uid, + ) { onProfileChange( profile.copy( uid = it, rootUseDefault = false ) ) - }) - UidPanel(uid = profile.gid, label = "gid", onUidChange = { + } + + SuperEditArrow( + title = "GID", + defaultValue = profile.gid, + ) { onProfileChange( profile.copy( gid = it, rootUseDefault = false ) ) - }) + + } val selectedGroups = profile.groups.ifEmpty { listOf(0) }.let { e -> e.mapNotNull { g -> Groups.entries.find { it.gid == g } } } + GroupsPanel(selectedGroups) { onProfileChange( profile.copy( @@ -155,15 +122,15 @@ fun RootProfileConfig( ) ) }) - } } -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun GroupsPanel(selected: List, closeSelection: (selection: Set) -> Unit) { - val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit -> - val groups = Groups.entries.toTypedArray().sortedWith( + val showDialog = remember { mutableStateOf(false) } + + val groups = remember { + Groups.entries.toTypedArray().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { when (it) { @@ -174,308 +141,257 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) } }) .then(compareBy { it.name }) - ) - val options = groups.map { value -> - ListOption( - titleText = value.display, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } - - val selection = HashSet(selected) - - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_groups), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options, - maxChoices = 32, // Kernel only supports 32 groups at most - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = groups[index] - selection.add(group) - } - } - ) - } } - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { + val currentSelection = remember { mutableStateOf(selected.toSet()) } - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectGroupsDialog.show() - } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_groups)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_groups), + summary = "${currentSelection.value.size} / 32", + insideMargin = DpSize(0.dp, 24.dp), + onDismissRequest = { showDialog.value = false } + ) { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + LazyColumn(modifier = Modifier.weight(1f, fill = false)) { + items(groups) { group -> + SuperCheckbox( + title = group.display, + summary = group.desc, + insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), + checkboxLocation = CheckboxLocation.Right, + checked = currentSelection.value.contains(group), + holdDownState = currentSelection.value.contains(group), + onCheckedChange = { isChecked -> + val newSelection = currentSelection.value.toMutableSet() + if (isChecked) { + if (newSelection.size < 32) newSelection.add(group) + } else { + newSelection.remove(group) + } + currentSelection.value = newSelection + } + ) } } + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { + currentSelection.value = selected.toSet() + showDialog.value = false + }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + closeSelection(currentSelection.value) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } } - } + + val tag = if (selected.isEmpty()) { + "None" + } else { + selected.joinToString(separator = ",", transform = { it.display }) + } + SuperArrow( + title = stringResource(R.string.profile_groups), + summary = tag, + onClick = { + showDialog.value = true + }, + ) + } -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun CapsPanel( selected: Collection, closeSelection: (selection: Set) -> Unit ) { - val selectCapabilitiesDialog = rememberCustomDialog { dismiss -> - val caps = Capabilities.entries.toTypedArray().sortedWith( + val showDialog = remember { mutableStateOf(false) } + + val caps = remember { + Capabilities.entries.toTypedArray().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { it.name }) ) - val options = caps.map { value -> - ListOption( - titleText = value.display, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } + } - val selection = HashSet(selected) + val currentSelection = remember { mutableStateOf(selected.toSet()) } - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_capabilities), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = caps[index] - selection.add(group) + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_capabilities), + insideMargin = DpSize(0.dp, 24.dp), + onDismissRequest = { showDialog.value = false }, + content = { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + LazyColumn(modifier = Modifier.weight(1f, fill = false)) { + items(caps) { cap -> + SuperCheckbox( + title = cap.display, + summary = cap.desc, + insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), + checkboxLocation = CheckboxLocation.Right, + checked = currentSelection.value.contains(cap), + holdDownState = currentSelection.value.contains(cap), + onCheckedChange = { isChecked -> + val newSelection = currentSelection.value.toMutableSet() + if (isChecked) { + newSelection.add(cap) + } else { + newSelection.remove(cap) + } + currentSelection.value = newSelection + } + ) } } - ) - } - } - - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectCapabilitiesDialog.show() - } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_capabilities)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { + showDialog.value = false + currentSelection.value = selected.toSet() + }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + closeSelection(currentSelection.value) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) } } } + ) + val tag = if (selected.isEmpty()) { + "None" + } else { + selected.joinToString(separator = ",", transform = { it.display }) } + SuperArrow( + title = stringResource(R.string.profile_capabilities), + summary = tag, + onClick = { + showDialog.value = true + } + ) + } -@Composable -private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { - - ListItem(headlineContent = { - var isError by remember { - mutableStateOf(false) - } - var lastValidUid by remember { - mutableIntStateOf(uid) - } - val keyboardController = LocalSoftwareKeyboardController.current - - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - value = uid.toString(), - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = { - if (it.isEmpty()) { - onUidChange(0) - return@OutlinedTextField - } - val valid = isTextValidUid(it) - - val targetUid = if (valid) it.toInt() else lastValidUid - if (valid) { - lastValidUid = it.toInt() - } - - onUidChange(targetUid) - - isError = !valid - } - ) - }) -} - -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SELinuxPanel( profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit ) { - val editSELinuxDialog = rememberCustomDialog { dismiss -> - var domain by remember { mutableStateOf(profile.context) } - var rules by remember { mutableStateOf(profile.rules) } + val showDialog = remember { mutableStateOf(false) } - val inputOptions = listOf( - InputTextField( - text = domain, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_domain), - ), - type = InputTextFieldType.OUTLINED, - required = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - imeAction = ImeAction.Next - ), - resultListener = { - domain = it ?: "" - }, - validationListener = { value -> - // value can be a-zA-Z0-9_ - val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") - if (value?.matches(regex) == true) ValidationResult.Valid - else ValidationResult.Invalid("Domain must be in the format of \"user:role:type:level\"") - } - ), - InputTextField( - text = rules, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_rules), - ), - type = InputTextFieldType.OUTLINED, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - ), - singleLine = false, - resultListener = { - rules = it ?: "" - }, - validationListener = { value -> - if (isSepolicyValid(value)) ValidationResult.Valid - else ValidationResult.Invalid("SELinux rules is invalid!") - } - ) - ) + var domain by remember { mutableStateOf(profile.context) } + var rules by remember { mutableStateOf(profile.rules) } + val isDomainValid = remember(domain) { + val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") + domain.matches(regex) + } + val isRulesValid = remember(rules) { isSepolicyValid(rules) } - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - InputDialog( - state = rememberUseCaseState( - visible = true, - onFinishedRequest = { - onSELinuxChange(domain, rules) - }, - onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_selinux_context), - ), - selection = InputSelection( - input = inputOptions, - onPositiveClick = { result -> - // Handle selection + SuperDialog( + show = showDialog, + title = stringResource(R.string.profile_selinux_context), + onDismissRequest = { showDialog.value = false } + ) { + Column(modifier = Modifier.heightIn(max = 500.dp)) { + Column(modifier = Modifier.weight(1f, fill = false)) { + TextField( + value = domain, + onValueChange = { domain = it }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + label = stringResource(id = R.string.profile_selinux_domain), + backgroundColor = colorScheme.surfaceContainer, + borderColor = if (isDomainValid) { + colorScheme.primary + } else { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + imeAction = ImeAction.Next + ), + singleLine = true ) - ) + TextField( + value = rules, + onValueChange = { rules = it }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + label = stringResource(id = R.string.profile_selinux_rules), + backgroundColor = colorScheme.surfaceContainer, + borderColor = if (isRulesValid) { + colorScheme.primary + } else { + Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + ), + singleLine = false + ) + } + Spacer(Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton( + onClick = { showDialog.value = false }, + text = stringResource(android.R.string.cancel), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(20.dp)) + TextButton( + onClick = { + onSELinuxChange(domain, rules) + showDialog.value = false + }, + text = stringResource(R.string.confirm), + enabled = isDomainValid && isRulesValid, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() + ) + } } } - ListItem(headlineContent = { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .clickable { - editSELinuxDialog.show() - }, - enabled = false, - colors = OutlinedTextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledBorderColor = MaterialTheme.colorScheme.outline, - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - label = { Text(text = stringResource(R.string.profile_selinux_context)) }, - value = profile.context, - onValueChange = { } - ) - }) -} - -@Preview -@Composable -private fun RootProfileConfigPreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - RootProfileConfig(fixedName = true, profile = profile) { - profile = it - } -} - -private fun isTextValidUid(text: String): Boolean { - return text.isNotEmpty() && text.isDigitsOnly() && text.toInt() >= 0 + SuperArrow( + title = stringResource(R.string.profile_selinux_context), + summary = profile.context, + onClick = { showDialog.value = true } + ) } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt index 7af311b3..19f99a64 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt @@ -1,105 +1,94 @@ package com.sukisu.ultra.ui.component.profile -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ReadMore -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp -import androidx.compose.material.icons.filled.Create -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.Create +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.sukisu.ultra.Natives import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.util.listAppProfileTemplates import com.sukisu.ultra.ui.util.setSepolicy import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.theme.MiuixTheme /** * @author weishu * @date 2023/10/21. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TemplateConfig( + modifier: Modifier = Modifier, profile: Natives.Profile, onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit ) { var expanded by remember { mutableStateOf(false) } - var template by rememberSaveable { - mutableStateOf(profile.rootTemplate ?: "") - } val profileTemplates = listAppProfileTemplates() val noTemplates = profileTemplates.isEmpty() - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - label = { Text(stringResource(R.string.profile_template)) }, - value = template.ifEmpty { "None" }, - onValueChange = {}, - trailingIcon = { - if (noTemplates) { - IconButton( - onClick = onManageTemplate - ) { - Icon(Icons.Filled.Create, null) - } - } else if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) + if (noTemplates) { + SuperArrow( + modifier = modifier, + title = stringResource(R.string.app_profile_template_create), + leftAction = { + Icon( + Icons.Rounded.Create, + null, + modifier = Modifier.padding(end = 16.dp), + tint = MiuixTheme.colorScheme.onBackground + ) + }, + onClick = onManageTemplate, + ) + } else { + var template by rememberSaveable { mutableStateOf(profile.rootTemplate ?: profileTemplates[0]) } + + Column(modifier = modifier) { + SuperDropdown( + title = stringResource(R.string.profile_template), + items = profileTemplates, + selectedIndex = profileTemplates.indexOf(template).takeIf { it >= 0 } ?: 0, + onSelectedIndexChange = { index -> + if (index < 0 || index >= profileTemplates.size) return@SuperDropdown + template = profileTemplates[index] + val templateInfo = getTemplateInfoById(template) + if (templateInfo != null && setSepolicy(template, templateInfo.rules.joinToString("\n"))) { + onProfileChange( + profile.copy( + rootTemplate = template, + rootUseDefault = false, + uid = templateInfo.uid, + gid = templateInfo.gid, + groups = templateInfo.groups, + capabilities = templateInfo.capabilities, + context = templateInfo.context, + namespace = templateInfo.namespace, + ) + ) + } }, + onClick = { + expanded = !expanded + }, + maxHeight = 280.dp + ) + SuperArrow( + title = stringResource(R.string.app_profile_template_view), + onClick = { onViewTemplate(template) } ) - if (profileTemplates.isEmpty()) { - return@ExposedDropdownMenuBox - } - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - profileTemplates.forEach { tid -> - val templateInfo = - getTemplateInfoById(tid) ?: return@forEach - DropdownMenuItem( - text = { Text(tid) }, - onClick = { - template = tid - if (setSepolicy(tid, templateInfo.rules.joinToString("\n"))) { - onProfileChange( - profile.copy( - rootTemplate = tid, - rootUseDefault = false, - uid = templateInfo.uid, - gid = templateInfo.gid, - groups = templateInfo.groups, - capabilities = templateInfo.capabilities, - context = templateInfo.context, - namespace = templateInfo.namespace, - ) - ) - } - expanded = false - }, - trailingIcon = { - IconButton(onClick = { - onViewTemplate(tid) - }) { - Icon(Icons.AutoMirrored.Filled.ReadMore, null) - } - } - ) - } - } } - }) + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/rebootListPopup.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/rebootListPopup.kt new file mode 100644 index 00000000..fe7b6f4d --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/rebootListPopup.kt @@ -0,0 +1,79 @@ +package com.sukisu.ultra.ui.component + +import android.content.Context +import android.os.Build +import android.os.PowerManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.screen.RebootDropdownItem +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Reboot +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme + +@Composable +fun RebootListPopup( + modifier: Modifier = Modifier, + alignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopRight +) { + val showTopPopup = remember { mutableStateOf(false) } + KsuIsValid { + IconButton( + modifier = modifier, + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.Reboot, + contentDescription = stringResource(id = R.string.reboot), + tint = colorScheme.onBackground + ) + } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = alignment, + onDismissRequest = { + showTopPopup.value = false + } + ) { + val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + + @Suppress("DEPRECATION") + val isRebootingUserspaceSupported = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true + + ListPopupColumn { + val rebootOptions = mutableListOf( + Pair(R.string.reboot, ""), + Pair(R.string.reboot_recovery, "recovery"), + Pair(R.string.reboot_bootloader, "bootloader"), + Pair(R.string.reboot_download, "download"), + Pair(R.string.reboot_edl, "edl") + ) + if (isRebootingUserspaceSupported) { + rebootOptions.add(1, Pair(R.string.reboot_userspace, "userspace")) + } + rebootOptions.forEachIndexed { idx, (id, reason) -> + RebootDropdownItem( + id = id, + reason = reason, + showTopPopup = showTopPopup, + optionSize = rebootOptions.size, + index = idx + ) + } + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/About.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/About.kt new file mode 100644 index 00000000..f161d14c --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/About.kt @@ -0,0 +1,207 @@ +package com.sukisu.ultra.ui.screen + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.FixedScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed +import com.kyant.capsule.ContinuousRoundedRectangle +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import com.sukisu.ultra.BuildConfig +import com.sukisu.ultra.R +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical + +@Composable +@Destination +fun AboutScreen(navigator: DestinationsNavigator) { + val uriHandler = LocalUriHandler.current + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) + + val htmlString = stringResource( + id = R.string.about_source_code, + "GitHub", + "Telegram", + "怡子曰曰", + "明风 OuO", + "CC BY-NC-SA 4.0" + ) + val result = extractLinks(htmlString) + + Scaffold( + topBar = { + TopAppBar( + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + title = stringResource(R.string.about), + navigationIcon = { + IconButton( + modifier = Modifier.padding(start = 16.dp), + onClick = dropUnlessResumed { navigator.popBackStack() } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .height(getWindowSize().height.dp) + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .hazeSource(state = hazeState) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, + ) { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(vertical = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(80.dp) + .clip(ContinuousRoundedRectangle(16.dp)) + .background(Color.White) + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "icon", + contentScale = FixedScale(1f) + ) + } + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.app_name), + fontWeight = FontWeight.Medium, + fontSize = 26.sp + ) + Text( + text = BuildConfig.VERSION_NAME, + fontSize = 14.sp + ) + } + } + item { + Card( + modifier = Modifier.padding(bottom = 12.dp) + ) { + result.forEach { + SuperArrow( + title = it.fullText, + onClick = { + uriHandler.openUri(it.url) + } + ) + } + } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } + } + } +} + +data class LinkInfo( + val fullText: String, + val url: String +) + +fun extractLinks(html: String): List { + val regex = Regex( + """([^<>\n\r]+?)\s*\s*]*\bhref\s*=\s*(['"]?)([^'"\s>]+)\2[^>]*>([^<]+)\s*\s*(.*?)\s*(?= + try { + val before = match.groupValues[1].trim() + val url = match.groupValues[3].trim() + val title = match.groupValues[4].trim() + val after = match.groupValues[5].trim() + + val fullText = "$before $title $after" + Log.d("ggc", "extractLinks: $fullText -> $url") + LinkInfo(fullText, url) + } catch (e: Exception) { + Log.e("ggc", "匹配失败: ${e.message}") + null + } + }.toList() +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt index 7764cb00..65499679 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt @@ -1,77 +1,140 @@ package com.sukisu.ultra.ui.screen -import android.annotation.SuppressLint -import androidx.annotation.StringRes -import androidx.compose.animation.* -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import android.os.Build +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +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.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Android -import androidx.compose.material.icons.filled.Security -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material.icons.rounded.Security +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed -import coil.compose.AsyncImage -import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +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.AppIconImage +import com.sukisu.ultra.ui.component.DropdownItem +import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.profile.AppProfileConfig import com.sukisu.ultra.ui.component.profile.RootProfileConfig import com.sukisu.ultra.ui.component.profile.TemplateConfig -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.* +import com.sukisu.ultra.ui.util.forceStopApp +import com.sukisu.ultra.ui.util.getSepolicy +import com.sukisu.ultra.ui.util.launchApp +import com.sukisu.ultra.ui.util.listAppProfileTemplates +import com.sukisu.ultra.ui.util.ownerNameForUid +import com.sukisu.ultra.ui.util.pickPrimary +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 +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.SmallTitle +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/5/16. */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun AppProfileScreen( navigator: DestinationsNavigator, appInfo: SuperUserViewModel.AppInfo, ) { val context = LocalContext.current - val snackBarHost = LocalSnackbarHost.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) val scope = rememberCoroutineScope() - val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label) + val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label).format(appInfo.uid) val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label) val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label) val packageName = appInfo.packageName + val sameUidApps = remember(appInfo.uid) { + SuperUserViewModel.apps.filter { it.uid == appInfo.uid } + } + val isUidGroup = sameUidApps.size > 1 + val primaryForIcon = remember(appInfo.uid, sameUidApps) { + runCatching { pickPrimary(sameUidApps) }.getOrNull() ?: appInfo + } + val sharedUserId = remember(appInfo.uid, sameUidApps, primaryForIcon) { + primaryForIcon.packageInfo.sharedUserId + ?: sameUidApps.firstOrNull { it.packageInfo.sharedUserId != null }?.packageInfo?.sharedUserId + ?: "" + } + val initialProfile = Natives.getAppProfile(packageName, appInfo.uid) if (initialProfile.allowSu) { initialProfile.rules = getSepolicy(packageName) @@ -80,77 +143,95 @@ fun AppProfileScreen( mutableStateOf(initialProfile) } - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - 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 + packageName = packageName, + showActions = !isUidGroup, + scrollBehavior = scrollBehavior, + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, - snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - AppProfileInner( + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + LazyColumn( modifier = Modifier - .padding(paddingValues) + .height(getWindowSize().height.dp) + .padding(top = 16.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()), - packageName = appInfo.packageName, - appLabel = appInfo.label, - appIcon = { - AsyncImage( - model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(), - contentDescription = appInfo.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) - ) - }, - profile = profile, - onViewTemplate = { - getTemplateInfoById(it)?.let { info -> - navigator.navigate(TemplateEditorScreenDestination(info)) - } - }, - onManageTemplate = { - navigator.navigate(AppProfileTemplateScreenDestination()) - }, - onProfileChange = { - scope.launch { - if (it.allowSu) { - // sync with allowlist.c - forbid_system_uid - if (appInfo.uid < 2000 && appInfo.uid != 1000) { - snackBarHost.showSnackbar(suNotAllowed) - return@launch - } - if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) { - snackBarHost.showSnackbar(failToUpdateSepolicy) - return@launch - } - } - if (!Natives.setAppProfile(it)) { - snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid)) + .hazeSource(state = hazeState), + contentPadding = innerPadding, + overscrollEffect = null + ) { + item { + AppProfileInner( + packageName = if (isUidGroup) "" else appInfo.packageName, + appLabel = if (isUidGroup) ownerNameForUid(appInfo.uid) else appInfo.label, + appIcon = { + val iconApp = if (isUidGroup) primaryForIcon else appInfo + AppIconImage( + packageInfo = iconApp.packageInfo, + label = iconApp.label, + modifier = Modifier.size(54.dp) + ) + }, + appUid = appInfo.uid, + sharedUserId = if (isUidGroup) sharedUserId else "", + appVersionName = if (isUidGroup) "" else (appInfo.packageInfo.versionName ?: ""), + appVersionCode = if (isUidGroup) 0L else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + appInfo.packageInfo.longVersionCode } else { - profile = it - } - } - }, - ) + @Suppress("DEPRECATION") + appInfo.packageInfo.versionCode.toLong() + }, + profile = profile, + isUidGroup = isUidGroup, + affectedApps = sameUidApps, + onViewTemplate = { + getTemplateInfoById(it)?.let { info -> + navigator.navigate(TemplateEditorScreenDestination(info)) { + launchSingleTop = true + } + } + }, + onManageTemplate = { + navigator.navigate(AppProfileTemplateScreenDestination()) { + launchSingleTop = true + } + }, + onProfileChange = { + scope.launch { + if (it.allowSu) { + if (appInfo.uid < 2000 && appInfo.uid != 1000) { + Toast.makeText(context, suNotAllowed, Toast.LENGTH_SHORT).show() + return@launch + } + if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) { + Toast.makeText(context, failToUpdateSepolicy, Toast.LENGTH_SHORT).show() + return@launch + } + } + if (!Natives.setAppProfile(it)) { + Toast.makeText(context, failToUpdateAppProfile, Toast.LENGTH_SHORT).show() + } else { + profile = it + } + } + }, + ) + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } + } } } @@ -159,243 +240,383 @@ private fun AppProfileInner( modifier: Modifier = Modifier, packageName: String, appLabel: String, - appIcon: @Composable () -> Unit, + appIcon: @Composable (() -> Unit), + appUid: Int, + sharedUserId: String = "", + appVersionName: String, + appVersionCode: Long, profile: Natives.Profile, + isUidGroup: Boolean = false, + affectedApps: List = emptyList(), onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit, ) { val isRootGranted = profile.allowSu - val cardColors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh) + val userId = appUid / 100000 + val appId = appUid % 100000 - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh - ) + Column( + modifier = modifier ) { - Column(modifier = modifier) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + insideMargin = PaddingValues(horizontal = 16.dp, vertical = 14.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically ) { - 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, + appIcon() + Column( + modifier = Modifier + .padding(start = 16.dp, end = 8.dp) + .weight(1f), + ) { + Text( + text = appLabel, + color = colorScheme.onSurface, + fontWeight = FontWeight(550), + modifier = Modifier + .basicMarquee(), + maxLines = 1, + softWrap = false ) + if (!isUidGroup) { + Text( + text = "$appVersionName ($appVersionCode)", + fontSize = 12.sp, + color = colorScheme.onSurfaceVariantSummary, + fontWeight = FontWeight.Medium, + modifier = Modifier + .basicMarquee(), + maxLines = 1, + softWrap = false + ) + Text( + text = packageName, + fontSize = 12.sp, + color = colorScheme.onSurfaceVariantSummary, + fontWeight = FontWeight.Medium, + modifier = Modifier + .basicMarquee(), + maxLines = 1, + softWrap = false + ) + } else { + if (sharedUserId.isNotEmpty()) { + Text( + text = sharedUserId, + fontSize = 12.sp, + color = colorScheme.onSurfaceVariantSummary, + fontWeight = FontWeight.Medium, + modifier = Modifier + .basicMarquee(), + maxLines = 1, + softWrap = false + ) + } + Text( + text = stringResource(R.string.group_contains_apps, affectedApps.size), + fontSize = 12.sp, + color = colorScheme.onSurfaceVariantSummary, + fontWeight = FontWeight.Medium, + modifier = Modifier + .basicMarquee(), + maxLines = 1, + softWrap = false + ) + } + } + Column( + modifier = Modifier, + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (userId != 0) { + StatusTag( + label = "USER $userId", + backgroundColor = colorScheme.primary.copy(alpha = 0.8f), + contentColor = colorScheme.onPrimary + ) + StatusTag( + label = "UID $appId", + backgroundColor = colorScheme.primary.copy(alpha = 0.8f), + contentColor = colorScheme.onPrimary + ) + } else { + StatusTag( + label = "UID $appUid", + backgroundColor = colorScheme.primary.copy(alpha = 0.8f), + contentColor = colorScheme.onPrimary + ) + } } } + } - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), - ) { - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.superuser), - checked = isRootGranted, - onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, - ) - } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + SuperSwitch( + leftAction = { + Icon( + imageVector = Icons.Rounded.Security, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = colorScheme.onBackground + ) + }, + title = stringResource(id = R.string.superuser), + checked = isRootGranted, + onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + ) + } - Crossfade( - targetState = isRootGranted, - label = "RootAccess" - ) { current -> - Column( - modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) - ) { - if (current) { - val initialMode = if (profile.rootUseDefault) { - Mode.Default - } else if (profile.rootTemplate != null) { - Mode.Template - } else { - Mode.Custom - } - var mode by rememberSaveable { - mutableStateOf(initialMode) - } + val initialRootMode = if (profile.rootUseDefault) { + Mode.Default + } else if (profile.rootTemplate != null) { + Mode.Template + } else { + Mode.Custom + } + var rootMode by rememberSaveable { + mutableStateOf(initialRootMode) + } + val nonRootMode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom + val dropdownMode = if (isRootGranted) rootMode else nonRootMode + ProfileBox(dropdownMode, isRootGranted) { mode -> + if (isRootGranted) { + when (mode) { + Mode.Default, Mode.Custom -> { + onProfileChange( + profile.copy( + rootUseDefault = mode == Mode.Default, + rootTemplate = null + ) + ) + rootMode = mode + } - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), - ) { - ProfileBox(mode, true) { - // template mode shouldn't change profile here! - if (it == Mode.Default || it == Mode.Custom) { - onProfileChange( - profile.copy( - rootUseDefault = it == Mode.Default, - rootTemplate = null - ) + Mode.Template -> { + val templates = listAppProfileTemplates() + if (templates.isNotEmpty()) { + val selected = profile.rootTemplate ?: templates[0] + val info = getTemplateInfoById(selected) + if (info != null && setSepolicy(selected, info.rules.joinToString("\n"))) { + onProfileChange( + profile.copy( + rootUseDefault = false, + rootTemplate = selected, + uid = info.uid, + gid = info.gid, + groups = info.groups, + capabilities = info.capabilities, + context = info.context, + namespace = info.namespace, ) - } - mode = it - } - } - - AnimatedVisibility( - visible = mode != Mode.Default, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Crossfade( - targetState = mode, - label = "ProfileMode" - ) { currentMode -> - when (currentMode) { - Mode.Template -> { - TemplateConfig( - profile = profile, - onViewTemplate = onViewTemplate, - onManageTemplate = onManageTemplate, - onProfileChange = onProfileChange - ) - } - - Mode.Custom -> { - RootProfileConfig( - fixedName = true, - profile = profile, - onProfileChange = onProfileChange - ) - } - - else -> {} - } - } - } - } - } - } else { - val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom - - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), - ) { - ProfileBox(mode, false) { - onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default))) - } - } - - AnimatedVisibility( - visible = mode == Mode.Custom, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium, - colors = cardColors, - elevation = getCardElevation(), - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - AppProfileConfig( - fixedName = true, - profile = profile, - enabled = mode == Mode.Custom, - onProfileChange = onProfileChange + ) + } else if (profile.rootTemplate != selected || profile.rootUseDefault) { + onProfileChange( + profile.copy( + rootUseDefault = false, + rootTemplate = selected ) - } + ) } + rootMode = Mode.Template } } } + } else { + onProfileChange(profile.copy(nonRootUseDefault = (mode == Mode.Default))) + } + } + Spacer(Modifier.height(12.dp)) + + AnimatedVisibility( + visible = isRootGranted, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = if (rootMode != Mode.Default) 12.dp else 0.dp), + ) { + AnimatedVisibility( + visible = rootMode == Mode.Template, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + TemplateConfig( + profile = profile, + onViewTemplate = onViewTemplate, + onManageTemplate = onManageTemplate, + onProfileChange = onProfileChange + ) + } + AnimatedVisibility( + visible = rootMode == Mode.Custom, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + RootProfileConfig( + fixedName = true, + profile = profile, + onProfileChange = onProfileChange + ) + } + } + } + AnimatedVisibility( + visible = !isRootGranted, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = if (nonRootMode != Mode.Default) 12.dp else 0.dp), + ) { + AnimatedVisibility( + visible = nonRootMode == Mode.Custom, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + AppProfileConfig( + fixedName = true, + profile = profile, + enabled = true, + onProfileChange = onProfileChange + ) + } + } + } + + if (isUidGroup) { + SmallTitle( + text = stringResource(R.string.app_profile_affects_following_apps), + modifier = Modifier.padding(top = 4.dp) + ) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + ) { + Spacer(Modifier.height(3.dp)) + affectedApps.forEach { app -> + BasicComponent( + leftAction = { + AppIconImage( + packageInfo = app.packageInfo, + label = app.label, + modifier = Modifier + .padding(end = 12.dp) + .size(40.dp) + ) + }, + title = app.label, + summary = app.packageName, + insideMargin = PaddingValues(horizontal = 16.dp, vertical = 12.dp) + ) + } + Spacer(Modifier.height(3.dp)) } } } } -private enum class Mode(@param:StringRes private val res: Int) { - Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom); - - val text: String - @Composable get() = stringResource(res) +private enum class Mode() { + Default, + Template, + Custom; } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( - title: String, - packageName: String, onBack: () -> Unit, - colors: TopAppBarColors, - scrollBehavior: TopAppBarScrollBehavior? = null + packageName: String, + showActions: Boolean = true, + scrollBehavior: ScrollBehavior, + hazeState: HazeState, + hazeStyle: HazeStyle, ) { TopAppBar( - title = { - Column { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - ) - Text( - text = packageName, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.alpha(0.8f) - ) - } + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f }, - colors = colors, + color = Color.Transparent, + title = stringResource(R.string.profile), navigationIcon = { IconButton( - onClick = onBack, + modifier = Modifier.padding(start = 16.dp), + onClick = onBack ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground ) } }, - 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, - ) + actions = { + if (showActions) { + val showTopPopup = remember { mutableStateOf(false) } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, + contentDescription = stringResource(id = R.string.settings) + ) + } + ListPopup( + show = showTopPopup, + onDismissRequest = { showTopPopup.value = false }, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + ) { + ListPopupColumn { + val items = listOf( + stringResource(id = R.string.launch_app), + stringResource(id = R.string.force_stop_app), + stringResource(id = R.string.restart_app) + ) + + items.forEachIndexed { index, text -> + DropdownItem( + text = text, + optionSize = items.size, + index = index, + onSelectedIndexChange = { selectedIndex -> + when (selectedIndex) { + 0 -> launchApp(packageName) + 1 -> forceStopApp(packageName) + 2 -> restartApp(packageName) + } + showTopPopup.value = false + } + ) + } + } + } + } + }, + scrollBehavior = scrollBehavior ) } @@ -405,182 +626,49 @@ private fun ProfileBox( hasTemplate: Boolean, onModeChange: (Mode) -> Unit, ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - ListItem( - headlineContent = { - Text( - text = stringResource(R.string.profile), - style = MaterialTheme.typography.titleMedium - ) - }, - supportingContent = { - Text( - text = mode.text, - style = MaterialTheme.typography.bodyMedium, - ) - }, - leadingContent = { + val defaultText = stringResource(R.string.profile_default) + val templateText = stringResource(R.string.profile_template) + val customText = stringResource(R.string.profile_custom) + val list = + remember(hasTemplate, defaultText, templateText, customText) { + buildList { + add(defaultText) + if (hasTemplate) { + add(templateText) + } + add(customText) + } + } + + val modesAndTitles = remember(hasTemplate, defaultText, templateText, customText) { + buildList { + add(Mode.Default to defaultText) + if (hasTemplate) { + add(Mode.Template to templateText) + } + add(Mode.Custom to customText) + } + } + val selectedIndex = modesAndTitles.indexOfFirst { it.first == mode } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + ) { + SuperDropdown( + title = stringResource(R.string.profile), + items = list, + leftAction = { Icon( - imageVector = Icons.Filled.AccountCircle, + Icons.Rounded.AccountCircle, + modifier = Modifier.padding(end = 16.dp), contentDescription = null, + tint = colorScheme.onBackground ) }, - ) - - HorizontalDivider( - thickness = Dp.Hairline, - ) - - ListItem( - headlineContent = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) - ) { - FilterChip( - selected = mode == Mode.Default, - onClick = { onModeChange(Mode.Default) }, - label = { - Text( - text = stringResource(R.string.profile_default), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - - if (hasTemplate) { - FilterChip( - selected = mode == Mode.Template, - onClick = { onModeChange(Mode.Template) }, - label = { - Text( - text = stringResource(R.string.profile_template), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - } - - FilterChip( - selected = mode == Mode.Custom, - onClick = { onModeChange(Mode.Custom) }, - label = { - Text( - text = stringResource(R.string.profile_custom), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - } - } - ) - } -} - -@SuppressLint("UnusedBoxWithConstraintsScope") -@Composable -private fun AppMenuBox( - packageName: String, - content: @Composable () -> Unit -) { - var expanded by remember { mutableStateOf(false) } - var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } - val density = LocalDensity.current - - BoxWithConstraints( - Modifier - .fillMaxSize() - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { - touchPoint = it - expanded = true - } - ) - } - ) { - content() - - val (offsetX, offsetY) = with(density) { - (touchPoint.x.toDp()) to (-touchPoint.y.toDp()) - } - - DropdownMenu( - expanded = expanded, - offset = DpOffset(offsetX, offsetY), - onDismissRequest = { - expanded = false - } + selectedIndex = if (selectedIndex == -1) 0 else selectedIndex, ) { - AppMenuOption( - text = stringResource(id = R.string.launch_app), - onClick = { - expanded = false - launchApp(packageName) - } - ) - - AppMenuOption( - text = stringResource(id = R.string.force_stop_app), - onClick = { - expanded = false - forceStopApp(packageName) - } - ) - - AppMenuOption( - text = stringResource(id = R.string.restart_app), - onClick = { - expanded = false - restartApp(packageName) - } - ) + onModeChange(modesAndTitles[it].first) } } } - -@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("")) } - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - Surface { - AppProfileInner( - packageName = "icu.nullptr.test", - appLabel = "Test", - appIcon = { - Icon( - imageVector = Icons.Filled.Android, - contentDescription = null, - ) - }, - profile = profile, - onProfileChange = { - profile = it - }, - ) - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt deleted file mode 100644 index 4175ecff..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import androidx.annotation.StringRes -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.* -import androidx.compose.ui.graphics.vector.ImageVector -import com.ramcosta.composedestinations.generated.destinations.* -import com.ramcosta.composedestinations.spec.DirectionDestinationSpec -import com.sukisu.ultra.R - -enum class BottomBarDestination( - val direction: DirectionDestinationSpec, - @param:StringRes val label: Int, - val iconSelected: ImageVector, - val iconNotSelected: ImageVector, - val rootRequired: Boolean, -) { - Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false), - Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true), - SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true), - Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, true), - Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false), -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt index 26359a94..b6fa6358 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt @@ -1,50 +1,86 @@ package com.sukisu.ultra.ui.screen import android.os.Environment -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.KeyEventBlocker -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.runModuleAction +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.KeyEventBlocker +import com.sukisu.ultra.ui.util.runModuleAction +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.SmallTopAppBar +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.io.File import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale @Composable @Destination fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String) { var text by rememberSaveable { mutableStateOf("") } - var tempText : String + var tempText: String val logContent = rememberSaveable { StringBuilder() } - val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() - var isActionRunning by rememberSaveable { mutableStateOf(true) } - - BackHandler(enabled = isActionRunning) { - // Disable back button if action is running - } + var actionResult: Boolean + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) LaunchedEffect(Unit) { if (text.isNotEmpty()) { @@ -65,83 +101,110 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String onStderr = { logContent.append(it).append("\n") } - ) + ).let { + actionResult = it + } } - isActionRunning = false + if (actionResult) navigator.popBackStack() } Scaffold( topBar = { TopBar( - isActionRunning = isActionRunning, + onBack = dropUnlessResumed { + navigator.popBackStack() + }, onSave = { - if (!isActionRunning) { - scope.launch { - val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) - val date = format.format(Date()) - val file = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "KernelSU_module_action_log_${date}.log" - ) - file.writeText(logContent.toString()) - snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") - } + scope.launch { + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) + val date = format.format(Date()) + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "KernelSU_module_action_log_${date}.log" + ) + file.writeText(logContent.toString()) + Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } - } + }, + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, - floatingActionButton = { - if (!isActionRunning) { - ExtendedFloatingActionButton( - text = { Text(text = stringResource(R.string.close)) }, - icon = { Icon(Icons.Filled.Close, contentDescription = null) }, - onClick = { - navigator.popBackStack() - } - ) - } - }, - contentWindowInsets = WindowInsets.safeDrawing, - snackbarHost = { SnackbarHost(snackBarHost) } + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize(1f) - .padding(innerPadding) + .scrollEndHaptic() + .hazeSource(state = hazeState) + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateStartPadding(layoutDirection), + ) .verticalScroll(scrollState), ) { LaunchedEffect(text) { scrollState.animateScrollTo(scrollState.maxValue) } + Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = text, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontSize = 12.sp, fontFamily = FontFamily.Monospace, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + Spacer( + Modifier.height( + 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) ) } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) { - TopAppBar( - title = { Text(stringResource(R.string.action)) }, - actions = { +private fun TopBar( + onBack: () -> Unit = {}, + onSave: () -> Unit = {}, + hazeState: HazeState, + hazeStyle: HazeStyle, +) { + SmallTopAppBar( + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + title = stringResource(R.string.action), + navigationIcon = { IconButton( - onClick = onSave, - enabled = !isActionRunning + modifier = Modifier.padding(start = 16.dp), + onClick = onBack ) { Icon( - imageVector = Icons.Filled.Save, + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + actions = { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { + Icon( + imageVector = MiuixIcons.Useful.Save, contentDescription = stringResource(id = R.string.save_log), + tint = colorScheme.onBackground ) } } ) -} +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index e991839c..ed7e5026 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -1,257 +1,132 @@ package com.sukisu.ultra.ui.screen -import android.content.Context -import android.content.Intent import android.net.Uri import android.os.Environment import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.animation.* -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import android.widget.Toast +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars 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.ArrowBack -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.activity.ComponentActivity +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination -import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.KeyEventBlocker -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.util.* -import com.sukisu.ultra.ui.viewmodel.ModuleViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.KeyEventBlocker +import com.sukisu.ultra.ui.util.FlashResult +import com.sukisu.ultra.ui.util.LkmSelection +import com.sukisu.ultra.ui.util.flashModule +import com.sukisu.ultra.ui.util.installBoot +import com.sukisu.ultra.ui.util.reboot +import com.sukisu.ultra.ui.util.restoreBoot +import com.sukisu.ultra.ui.util.uninstallPermanently +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.SmallTopAppBar +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.io.File import java.text.SimpleDateFormat -import java.util.* -import androidx.core.content.edit -import com.sukisu.ultra.ui.util.module.ModuleOperationUtils -import com.sukisu.ultra.ui.util.module.ModuleUtils +import java.util.Date +import java.util.Locale /** - * @author ShirkNeko - * @date 2025/5/31. + * @author weishu + * @date 2023/1/1. */ + enum class FlashingStatus { FLASHING, SUCCESS, FAILED } -private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING) - -// 添加模块安装状态跟踪 -data class ModuleInstallStatus( - val totalModules: Int = 0, - val currentModule: Int = 0, - val currentModuleName: String = "", - val failedModules: MutableList = mutableListOf(), - val verifiedModules: MutableList = mutableListOf() // 添加已验证模块列表 -) - -private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus()) - -// 存储模块URI和验证状态的映射 -private var moduleVerificationMap = mutableMapOf() - -fun setFlashingStatus(status: FlashingStatus) { - currentFlashingStatus.value = status -} - -fun updateModuleInstallStatus( - totalModules: Int? = null, - currentModule: Int? = null, - currentModuleName: String? = null, - failedModule: String? = null, - verifiedModule: String? = null -) { - val current = moduleInstallStatus.value - moduleInstallStatus.value = current.copy( - totalModules = totalModules ?: current.totalModules, - currentModule = currentModule ?: current.currentModule, - currentModuleName = currentModuleName ?: current.currentModuleName - ) - - if (failedModule != null) { - val updatedFailedModules = current.failedModules.toMutableList() - updatedFailedModules.add(failedModule) - moduleInstallStatus.value = moduleInstallStatus.value.copy( - failedModules = updatedFailedModules - ) - } - - if (verifiedModule != null) { - val updatedVerifiedModules = current.verifiedModules.toMutableList() - updatedVerifiedModules.add(verifiedModule) - moduleInstallStatus.value = moduleInstallStatus.value.copy( - verifiedModules = updatedVerifiedModules - ) - } -} - -fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) { - moduleVerificationMap[uri] = isVerified -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -@Destination -fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { - val context = LocalContext.current - - val shouldAutoExit = remember { - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.getBoolean("auto_exit_after_flash", false) - } - - // 是否通过从外部启动的模块安装 - val isExternalInstall = remember { - when (flashIt) { - is FlashIt.FlashModule -> { - (context as? ComponentActivity)?.intent?.let { intent -> - intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND - } ?: false +// Lets you flash modules sequentially when mutiple zipUris are selected +fun flashModulesSequentially( + uris: List, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit +): FlashResult { + for (uri in uris) { + flashModule(uri, onStdout, onStderr).apply { + if (code != 0) { + return FlashResult(code, err, showReboot) } - is FlashIt.FlashModules -> { - (context as? ComponentActivity)?.intent?.let { intent -> - intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND - } ?: false - } - else -> false } } + return FlashResult(0, "", true) +} +@Composable +@Destination +fun FlashScreen( + navigator: DestinationsNavigator, + flashIt: FlashIt +) { var text by rememberSaveable { mutableStateOf("") } var tempText: String val logContent = rememberSaveable { StringBuilder() } var showFloatAction by rememberSaveable { mutableStateOf(false) } - // 添加状态跟踪是否已经完成刷写 - var hasFlashCompleted by rememberSaveable { mutableStateOf(false) } - var hasExecuted by rememberSaveable { mutableStateOf(false) } - // 更新模块状态管理 - var hasUpdateExecuted by rememberSaveable { mutableStateOf(false) } - var hasUpdateCompleted by rememberSaveable { mutableStateOf(false) } - val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current val scope = rememberCoroutineScope() val scrollState = rememberScrollState() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val viewModel: ModuleViewModel = viewModel() - - val errorCodeString = stringResource(R.string.error_code) - val checkLogString = stringResource(R.string.check_log) - val logSavedString = stringResource(R.string.log_saved) - val installingModuleString = stringResource(R.string.installing_module) - - // 当前模块安装状态 - val currentStatus = moduleInstallStatus.value - - // 重置状态 - LaunchedEffect(flashIt) { - when (flashIt) { - is FlashIt.FlashModules -> { - if (flashIt.currentIndex == 0) { - moduleInstallStatus.value = ModuleInstallStatus( - totalModules = flashIt.uris.size, - currentModule = 1 - ) - hasFlashCompleted = false - hasExecuted = false - moduleVerificationMap.clear() - } - } - is FlashIt.FlashModuleUpdate -> { - hasUpdateCompleted = false - hasUpdateExecuted = false - } - else -> { - hasFlashCompleted = false - hasExecuted = false - } - } + var flashing by rememberSaveable { + mutableStateOf(FlashingStatus.FLASHING) } - // 处理更新模块安装 - LaunchedEffect(flashIt) { - if (flashIt !is FlashIt.FlashModuleUpdate) return@LaunchedEffect - if (hasUpdateExecuted || hasUpdateCompleted || text.isNotEmpty()) { + LaunchedEffect(Unit) { + if (text.isNotEmpty()) { return@LaunchedEffect } - - hasUpdateExecuted = true - withContext(Dispatchers.IO) { - setFlashingStatus(FlashingStatus.FLASHING) - - try { - logContent.append(text).append("\n") - } catch (_: Exception) { - logContent.append(text).append("\n") - } - - flashModuleUpdate(flashIt.uri, onFinish = { showReboot, code -> - if (code != 0) { - text += "$errorCodeString $code.\n$checkLogString\n" - setFlashingStatus(FlashingStatus.FAILED) - } else { - setFlashingStatus(FlashingStatus.SUCCESS) - - // 处理模块更新成功后的验证标志 - val isVerified = moduleVerificationMap[flashIt.uri] ?: false - ModuleOperationUtils.handleModuleUpdate(context, flashIt.uri, isVerified) - - viewModel.markNeedRefresh() - } - if (showReboot) { - text += "\n\n\n" - showFloatAction = true - - // 如果是内部安装,显示重启按钮后不自动返回 - if (isExternalInstall) { - return@flashModuleUpdate - } - } - hasUpdateCompleted = true - - // 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回 - if (isExternalInstall || shouldAutoExit) { - scope.launch { - kotlinx.coroutines.delay(1000) - if (shouldAutoExit) { - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.edit { remove("auto_exit_after_flash") } - } - (context as? ComponentActivity)?.finish() - } - } - }, onStdout = { + flashIt(flashIt, onStdout = { tempText = "$it\n" if (tempText.startsWith("")) { // clear command text = tempText.substring(6) @@ -261,156 +136,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { logContent.append(it).append("\n") }, onStderr = { logContent.append(it).append("\n") - }) - } - } - - // 安装但排除更新模块 - LaunchedEffect(flashIt) { - if (flashIt is FlashIt.FlashModuleUpdate) return@LaunchedEffect - if (hasExecuted || hasFlashCompleted || text.isNotEmpty()) { - return@LaunchedEffect - } - - hasExecuted = true - - withContext(Dispatchers.IO) { - setFlashingStatus(FlashingStatus.FLASHING) - - if (flashIt is FlashIt.FlashModules) { - try { - val currentUri = flashIt.uris[flashIt.currentIndex] - val moduleName = getModuleNameFromUri(context, currentUri) - updateModuleInstallStatus( - currentModuleName = moduleName - ) - text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName) - logContent.append(text).append("\n") - } catch (_: Exception) { - text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module") - logContent.append(text).append("\n") - } - } - - flashIt(flashIt, onFinish = { showReboot, code -> + }).apply { if (code != 0) { - text += "$errorCodeString $code.\n$checkLogString\n" - setFlashingStatus(FlashingStatus.FAILED) - - if (flashIt is FlashIt.FlashModules) { - updateModuleInstallStatus( - failedModule = moduleInstallStatus.value.currentModuleName - ) - } - } else { - setFlashingStatus(FlashingStatus.SUCCESS) - - // 处理模块安装成功后的验证标志 - when (flashIt) { - is FlashIt.FlashModule -> { - val isVerified = moduleVerificationMap[flashIt.uri] ?: false - ModuleOperationUtils.handleModuleInstallSuccess(context, flashIt.uri, isVerified) - if (isVerified) { - updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName) - } - } - is FlashIt.FlashModules -> { - val currentUri = flashIt.uris[flashIt.currentIndex] - val isVerified = moduleVerificationMap[currentUri] ?: false - ModuleOperationUtils.handleModuleInstallSuccess(context, currentUri, isVerified) - if (isVerified) { - updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName) - } - } - - else -> {} - } - - viewModel.markNeedRefresh() + text += "Error code: $code.\n $err Please save and check the log.\n" } if (showReboot) { text += "\n\n\n" showFloatAction = true } - - hasFlashCompleted = true - - if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) { - val nextFlashIt = flashIt.copy( - currentIndex = flashIt.currentIndex + 1 - ) - scope.launch { - kotlinx.coroutines.delay(500) - navigator.navigate(FlashScreenDestination(nextFlashIt)) - } - } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) { - // 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回 - scope.launch { - kotlinx.coroutines.delay(1000) - if (shouldAutoExit) { - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.edit { remove("auto_exit_after_flash") } - } - (context as? ComponentActivity)?.finish() - } - } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) { - // 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回 - scope.launch { - kotlinx.coroutines.delay(1000) - if (shouldAutoExit) { - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.edit { remove("auto_exit_after_flash") } - } - (context as? ComponentActivity)?.finish() - } - } - }, onStdout = { - tempText = "$it\n" - if (tempText.startsWith("")) { // clear command - text = tempText.substring(6) - } else { - text += tempText - } - logContent.append(it).append("\n") - }, onStderr = { - logContent.append(it).append("\n") - }) - } - } - - val onBack: () -> Unit = { - val canGoBack = when (flashIt) { - is FlashIt.FlashModuleUpdate -> currentFlashingStatus.value != FlashingStatus.FLASHING - else -> currentFlashingStatus.value != FlashingStatus.FLASHING - } - - if (canGoBack) { - if (isExternalInstall) { - (context as? ComponentActivity)?.finish() - } else { - if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.navigate(ModuleScreenDestination) - } else { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.popBackStack() - } + flashing = if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED } } } - BackHandler(enabled = true) { - onBack() - } - Scaffold( topBar = { TopBar( - currentFlashingStatus.value, - currentStatus, - onBack = onBack, + flashing, + onBack = dropUnlessResumed { navigator.popBackStack() }, onSave = { scope.launch { val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) @@ -420,15 +163,22 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { "KernelSU_install_log_${date}.log" ) file.writeText(logContent.toString()) - snackBarHost.showSnackbar(logSavedString.format(file.absolutePath)) + Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } }, - scrollBehavior = scrollBehavior ) }, floatingActionButton = { if (showFloatAction) { - ExtendedFloatingActionButton( + val reboot = stringResource(id = R.string.reboot) + FloatingActionButton( + modifier = Modifier + .padding( + bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, + end = 20.dp + ) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), onClick = { scope.launch { withContext(Dispatchers.IO) { @@ -436,25 +186,22 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } } }, - icon = { + shadowElevation = 0.dp, + content = { Icon( - Icons.Filled.Refresh, - contentDescription = stringResource(id = R.string.reboot) + Icons.Rounded.Refresh, + reboot, + Modifier.size(40.dp), + tint = Color.White ) }, - text = { - Text(text = stringResource(id = R.string.reboot)) - }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - expanded = true ) } }, - snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - containerColor = MaterialTheme.colorScheme.background + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } @@ -462,307 +209,107 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { Column( modifier = Modifier .fillMaxSize(1f) - .padding(innerPadding) - .nestedScroll(scrollBehavior.nestedScrollConnection), + .scrollEndHaptic() + .padding( + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateStartPadding(layoutDirection), + ) + .verticalScroll(scrollState), ) { - if (flashIt is FlashIt.FlashModules) { - ModuleInstallProgressBar( - currentIndex = flashIt.currentIndex + 1, - totalCount = flashIt.uris.size, - currentModuleName = currentStatus.currentModuleName, - status = currentFlashingStatus.value, - failedModules = currentStatus.failedModules - ) - - Spacer(modifier = Modifier.height(8.dp)) + LaunchedEffect(text) { + scrollState.animateScrollTo(scrollState.maxValue) } - - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(scrollState) - ) { - LaunchedEffect(text) { - scrollState.animateScrollTo(scrollState.maxValue) - } - Text( - modifier = Modifier.padding(16.dp), - text = text, - style = MaterialTheme.typography.bodyMedium, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } -} - -// 显示模块安装进度条和状态 -@Composable -fun ModuleInstallProgressBar( - currentIndex: Int, - totalCount: Int, - currentModuleName: String, - status: FlashingStatus, - failedModules: List -) { - val progressColor = when(status) { - FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary - FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary - FlashingStatus.FAILED -> MaterialTheme.colorScheme.error - } - - val progress = animateFloatAsState( - targetValue = currentIndex.toFloat() / totalCount.toFloat(), - label = "InstallProgress" - ) - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - // 模块名称和进度 - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = currentModuleName.ifEmpty { stringResource(R.string.module) }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - Text( - text = "$currentIndex/$totalCount", - style = MaterialTheme.typography.titleMedium - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - // 进度条 - LinearProgressIndicator( - progress = { progress.value }, - modifier = Modifier - .fillMaxWidth() - .height(8.dp), - color = progressColor, - trackColor = MaterialTheme.colorScheme.surfaceVariant + Spacer(Modifier.height(innerPadding.calculateTopPadding())) + Text( + modifier = Modifier.padding(8.dp), + text = text, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 失败模块列表 - AnimatedVisibility( - visible = failedModules.isNotEmpty(), - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = stringResource(R.string.module_failed_count, failedModules.size), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - // 失败模块列表 - Column( - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), - shape = MaterialTheme.shapes.small - ) - .padding(8.dp) - ) { - failedModules.forEach { moduleName -> - Text( - text = "• $moduleName", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } - } - } - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - status: FlashingStatus, - moduleStatus: ModuleInstallStatus = ModuleInstallStatus(), - onBack: () -> Unit, - onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - val cardAlpha = CardConfig.cardAlpha - - val statusColor = when(status) { - FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary - FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary - FlashingStatus.FAILED -> MaterialTheme.colorScheme.error - } - - TopAppBar( - title = { - Column { - Text( - text = stringResource( - when (status) { - FlashingStatus.FLASHING -> R.string.flashing - FlashingStatus.SUCCESS -> R.string.flash_success - FlashingStatus.FAILED -> R.string.flash_failed - } - ), - style = MaterialTheme.typography.titleLarge, - color = statusColor + Spacer( + Modifier.height( + 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) - - if (moduleStatus.failedModules.isNotEmpty()) { - Text( - text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - } - }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - actions = { - IconButton(onClick = onSave) { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = stringResource(id = R.string.save_log), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} - -suspend fun getModuleNameFromUri(context: Context, uri: Uri): String { - return withContext(Dispatchers.IO) { - try { - if (uri == Uri.EMPTY) { - return@withContext context.getString(R.string.unknown_module) - } - if (!ModuleUtils.isUriAccessible(context, uri)) { - return@withContext context.getString(R.string.unknown_module) - } - ModuleUtils.extractModuleName(context, uri) - } catch (_: Exception) { - context.getString(R.string.unknown_module) + ) } } } @Parcelize sealed class FlashIt : Parcelable { - data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt() - data class FlashModule(val uri: Uri) : FlashIt() - data class FlashModules(val uris: List, val currentIndex: Int = 0) : FlashIt() - data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新 - data object FlashRestore : FlashIt() - data object FlashUninstall : FlashIt() -} + data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : + FlashIt() -// 模块更新刷写 -fun flashModuleUpdate( - uri: Uri, - onFinish: (Boolean, Int) -> Unit, - onStdout: (String) -> Unit, - onStderr: (String) -> Unit -) { - flashModule(uri, onFinish, onStdout, onStderr) + data class FlashModules(val uris: List) : FlashIt() + + data object FlashRestore : FlashIt() + + data object FlashUninstall : FlashIt() } fun flashIt( flashIt: FlashIt, - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit -) { - when (flashIt) { +): FlashResult { + return when (flashIt) { is FlashIt.FlashBoot -> installBoot( flashIt.boot, flashIt.lkm, flashIt.ota, flashIt.partition, - onFinish, onStdout, onStderr ) - is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr) + is FlashIt.FlashModules -> { - if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) { - onFinish(false, 0) - return - } - - val currentUri = flashIt.uris[flashIt.currentIndex] - onStdout("\n") - - flashModule(currentUri, onFinish, onStdout, onStderr) + flashModulesSequentially(flashIt.uris, onStdout, onStderr) } - is FlashIt.FlashModuleUpdate -> { - onFinish(false, 0) - } - FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr) - FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr) + + FlashIt.FlashRestore -> restoreBoot(onStdout, onStderr) + + FlashIt.FlashUninstall -> uninstallPermanently(onStdout, onStderr) } } -@Preview @Composable -fun FlashScreenPreview() { - FlashScreen(EmptyDestinationsNavigator, FlashIt.FlashUninstall) -} \ No newline at end of file +private fun TopBar( + status: FlashingStatus, + onBack: () -> Unit = {}, + onSave: () -> Unit = {}, +) { + SmallTopAppBar( + title = stringResource( + when (status) { + FlashingStatus.FLASHING -> R.string.flashing + FlashingStatus.SUCCESS -> R.string.flash_success + FlashingStatus.FAILED -> R.string.flash_failed + } + ), + navigationIcon = { + IconButton( + modifier = Modifier.padding(start = 16.dp), + onClick = onBack + ) { + Icon( + MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + actions = { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { + Icon( + imageVector = MiuixIcons.Useful.Save, + contentDescription = stringResource(id = R.string.save_log), + tint = colorScheme.onBackground + ) + } + }, + ) +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index b6ef7128..c382f6a1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -1,211 +1,203 @@ package com.sukisu.ultra.ui.screen -import android.annotation.SuppressLint import android.content.Context import android.os.Build -import android.os.PowerManager import android.system.Os import androidx.annotation.StringRes -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +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.add +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.PagerState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.Block -import androidx.compose.material.icons.outlined.TaskAlt -import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.CheckCircleOutline +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material.icons.rounded.Link +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.content.pm.PackageInfoCompat -import androidx.lifecycle.viewmodel.compose.viewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination -import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.Natives import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.KsuIsValid +import com.sukisu.ultra.getKernelVersion +import com.sukisu.ultra.ui.component.DropdownItem +import com.sukisu.ultra.ui.component.RebootListPopup import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.theme.CardConfig -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.susfs.util.SuSFSManager import com.sukisu.ultra.ui.util.checkNewVersion -import com.sukisu.ultra.ui.util.getSuSFSVersion +import com.sukisu.ultra.ui.util.getModuleCount +import com.sukisu.ultra.ui.util.getSELinuxStatus +import com.sukisu.ultra.ui.util.getSuperuserCount import com.sukisu.ultra.ui.util.module.LatestVersionInfo import com.sukisu.ultra.ui.util.reboot -import com.sukisu.ultra.ui.viewmodel.HomeViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.random.Random +import com.sukisu.ultra.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.CardDefaults +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Save +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -/** - * @author ShirkNeko - * @date 2025/9/29. - */ -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) -@Destination(start = true) @Composable -fun HomeScreen(navigator: DestinationsNavigator) { - val context = LocalContext.current - val viewModel = viewModel() - val coroutineScope = rememberCoroutineScope() - - val pullRefreshState = rememberPullRefreshState( - refreshing = viewModel.isRefreshing, - onRefresh = { - viewModel.onPullRefresh(context) - } +fun HomePager( + pagerState: PagerState, + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { + val kernelVersion = getKernelVersion() + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) ) - LaunchedEffect(key1 = navigator) { - viewModel.loadUserSettings(context) - coroutineScope.launch { - viewModel.loadCoreData() - delay(100) - viewModel.loadExtendedData(context) - } - - // 启动数据变化监听 - coroutineScope.launch { - while (true) { - delay(5000) // 每5秒检查一次 - viewModel.autoRefreshIfNeeded(context) - } - } - } - - // 监听数据刷新状态流 - LaunchedEffect(viewModel.dataRefreshTrigger) { - viewModel.dataRefreshTrigger.collect { _ -> - // 数据刷新时的额外处理可以在这里添加 - } - } - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val scrollState = rememberScrollState() + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val checkUpdate = prefs.getBoolean("check_update", true) Scaffold( topBar = { TopBar( + kernelVersion = kernelVersion, + onInstallClick = { + navigator.navigate(InstallScreenDestination) { + popUpTo(InstallScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + }, scrollBehavior = scrollBehavior, - navigator = navigator, - isDataLoaded = viewModel.isCoreDataLoaded + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Box( + LazyColumn( modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .pullRefresh(pullRefreshState) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .padding(horizontal = 12.dp) + .hazeSource(state = hazeState), + contentPadding = innerPadding, + overscrollEffect = null, ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(top = 12.dp, start = 16.dp, end = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 状态卡片 - if (viewModel.isCoreDataLoaded) { - StatusCard( - systemStatus = viewModel.systemStatus, - onClickInstall = { - navigator.navigate(InstallScreenDestination(preselectedKernelUri = null)) - } - ) + item { + val coroutineScope = rememberCoroutineScope() + val isManager = Natives.isManager + val ksuVersion = if (isManager) Natives.version else null + val lkmMode = ksuVersion?.let { + if (kernelVersion.isGKI()) Natives.isLkmMode else null + } - // 警告信息 - if (viewModel.systemStatus.requireNewKernel) { + Column( + modifier = Modifier.padding(vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (isManager && Natives.requireNewKernel()) { WarningCard( stringResource(id = R.string.require_kernel_version).format( - Natives.getSimpleVersionFull(), - Natives.MINIMAL_SUPPORTED_KERNEL_FULL + ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL ) ) } - - if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) { + if (ksuVersion != null && !rootAvailable()) { WarningCard( stringResource(id = R.string.grant_root_failed) ) } + StatusCard( + kernelVersion, ksuVersion, lkmMode, + onClickInstall = { + navigator.navigate(InstallScreenDestination) { + launchSingleTop = true + } + }, + onClickSuperuser = { + coroutineScope.launch { + pagerState.animateScrollToPage(1) + } + }, + onclickModule = { + coroutineScope.launch { + pagerState.animateScrollToPage(2) + } + } + ) - // 只有在没有其他警告信息时才显示不兼容内核警告 - val shouldShowWarnings = viewModel.systemStatus.requireNewKernel || - (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) - - if (Natives.version <= Natives.MINIMAL_NEW_IOCTL_KERNEL && !shouldShowWarnings && viewModel.systemStatus.ksuVersion != null) { - IncompatibleKernelCard() - Spacer(Modifier.height(12.dp)) - } - } - - // 更新检查 - if (viewModel.isExtendedDataLoaded) { - val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("check_update", true) if (checkUpdate) { UpdateCard() } - - // 信息卡片 - InfoCard( - systemInfo = viewModel.systemInfo, - isSimpleMode = viewModel.isSimpleMode, - isHideSusfsStatus = viewModel.isHideSusfsStatus, - isHideZygiskImplement = viewModel.isHideZygiskImplement, - showKpmInfo = viewModel.showKpmInfo, - lkmMode = viewModel.systemStatus.lkmMode, - ) - - // 链接卡片 - if (!viewModel.isSimpleMode && !viewModel.isHideLinkCard) { - ContributionCard() - DonateCard() - LearnMoreCard() - } + InfoCard() + DonateCard() + LearnMoreCard() } - - if (!viewModel.isExtendedDataLoaded) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(bottomInnerPadding)) } } } @@ -232,287 +224,280 @@ fun UpdateCard() { AnimatedVisibility( visible = newVersionCode > currentVersionCode, - enter = fadeIn() + expandVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ) - ), + enter = fadeIn() + expandVertically(), exit = shrinkVertically() + fadeOut() ) { val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) WarningCard( message = stringResource(id = R.string.new_version_available).format(newVersionCode), - color = MaterialTheme.colorScheme.outlineVariant, - onClick = { - if (changelog.isEmpty()) { - uriHandler.openUri(newVersionUrl) - } else { - updateDialog.showConfirm( - title = title, - content = changelog, - markdown = true, - confirm = updateText + colorScheme.outline + ) { + if (changelog.isEmpty()) { + uriHandler.openUri(newVersionUrl) + } else { + updateDialog.showConfirm( + title = title, + content = changelog, + markdown = true, + confirm = updateText + ) + } + } + } +} + +@Composable +fun RebootDropdownItem( + @StringRes id: Int, reason: String = "", + showTopPopup: MutableState, + optionSize: Int, + index: Int, +) { + DropdownItem( + text = stringResource(id), + optionSize = optionSize, + onSelectedIndexChange = { + reboot(reason) + showTopPopup.value = false + }, + index = index + ) +} + +@Composable +private fun TopBar( + kernelVersion: KernelVersion, + onInstallClick: () -> Unit, + scrollBehavior: ScrollBehavior, + hazeState: HazeState, + hazeStyle: HazeStyle, +) { + TopAppBar( + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + title = stringResource(R.string.app_name), + actions = { + if (kernelVersion.isGKI()) { + IconButton( + modifier = Modifier.padding(end = 8.dp), + onClick = onInstallClick, + ) { + Icon( + imageVector = MiuixIcons.Useful.Save, + contentDescription = stringResource(id = R.string.install), + tint = colorScheme.onBackground ) } } - ) - } -} - -@Composable -fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { - DropdownMenuItem( - text = { Text(stringResource(id)) }, - onClick = { reboot(reason) }) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - scrollBehavior: TopAppBarScrollBehavior? = null, - navigator: DestinationsNavigator, - isDataLoaded: Boolean = false -) { - val context = LocalContext.current - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - - TopAppBar( - title = { - Text( - text = stringResource(R.string.app_name), - style = MaterialTheme.typography.titleLarge + RebootListPopup( + modifier = Modifier.padding(end = 16.dp), ) }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - actions = { - if (isDataLoaded) { - // SuSFS 配置按钮 - val susfsVersion = getSuSFSVersion() - if (susfsVersion.isNotEmpty() && !susfsVersion.startsWith("[-]") && SuSFSManager.isBinaryAvailable(context)) { - IconButton(onClick = { - navigator.navigate(SuSFSConfigScreenDestination) - }) { - Icon( - imageVector = Icons.Filled.Tune, - contentDescription = stringResource(R.string.susfs_config_setting_title) - ) - } - } - - // 重启按钮 - var showDropdown by remember { mutableStateOf(false) } - KsuIsValid { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Filled.PowerSettingsNew, - contentDescription = stringResource(id = R.string.reboot) - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - RebootDropdownItem(id = R.string.reboot) - - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") - } - RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") - RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") - RebootDropdownItem(id = R.string.reboot_download, reason = "download") - RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") - } - } - } - } - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @Composable private fun StatusCard( - systemStatus: HomeViewModel.SystemStatus, - onClickInstall: () -> Unit = {} + kernelVersion: KernelVersion, + ksuVersion: Int?, + lkmMode: Boolean?, + onClickInstall: () -> Unit = {}, + onClickSuperuser: () -> Unit = {}, + onclickModule: () -> Unit = {}, ) { - ElevatedCard( - colors = getCardColors( - if (systemStatus.ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer - else MaterialTheme.colorScheme.errorContainer - ), - elevation = getCardElevation(), + Column( + modifier = Modifier ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - if (systemStatus.isRootAvailable || systemStatus.kernelVersion.isGKI()) { - onClickInstall() - } + when { + ksuVersion != null -> { + val safeMode = when { + Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]" + else -> "" } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - when { - systemStatus.ksuVersion != null -> { - val workingModeText = when { - Natives.isSafeMode -> stringResource(id = R.string.safe_mode) - else -> stringResource(id = R.string.home_working) - } + val workingMode = when (lkmMode) { + null -> "" + true -> " " + else -> " " + } - val workingModeSurfaceText = when { - systemStatus.lkmMode == true -> "LKM" - else -> "Built-in" - } + val workingText = "${stringResource(id = R.string.home_working)}$workingMode$safeMode" - Icon( - Icons.Outlined.TaskAlt, - contentDescription = stringResource(R.string.home_working), - tint = MaterialTheme.colorScheme.primary, + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Card( modifier = Modifier - .size(28.dp) - .padding( - horizontal = 4.dp - ), - ) - - Column(Modifier.padding(start = 20.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + .weight(1f) + .fillMaxHeight(), + colors = CardDefaults.defaultColors( + color = if (isSystemInDarkTheme()) Color(0xFF1A3825) else Color(0xFFDFFAE4) + ), + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Box( + modifier = Modifier.fillMaxSize() ) { - Text( - text = workingModeText, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - ) - - Spacer(Modifier.width(8.dp)) - - // 工作模式标签 - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primary, + Box( modifier = Modifier + .fillMaxSize() + .offset(38.dp, 45.dp), + contentAlignment = Alignment.BottomEnd + ) { + Icon( + modifier = Modifier.size(170.dp), + imageVector = Icons.Rounded.CheckCircleOutline, + tint = Color(0xFF36D167), + contentDescription = null + ) + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 16.dp) ) { Text( - text = workingModeSurfaceText, - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.onPrimary + modifier = Modifier.fillMaxWidth(), + text = workingText, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, ) - } - - Spacer(Modifier.width(6.dp)) - - // 架构标签 - if (Os.uname().machine != "aarch64") { - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - ) { - Text( - text = Os.uname().machine, - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding( - horizontal = 6.dp, - vertical = 2.dp - ), - color = MaterialTheme.colorScheme.onPrimary - ) - } - } - } - - val isHideVersion = LocalContext.current.getSharedPreferences( - "settings", - Context.MODE_PRIVATE - ) - .getBoolean("is_hide_version", false) - - if (!isHideVersion) { - Spacer(Modifier.height(4.dp)) - systemStatus.ksuFullVersion?.let { + Spacer(Modifier.height(2.dp)) Text( - text = stringResource(R.string.home_working_version, it), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.home_working_version, ksuVersion), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + ) + } + } + } + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + insideMargin = PaddingValues(16.dp), + onClick = { onClickSuperuser() }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.superuser), + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = getSuperuserCount().toString(), + fontSize = 26.sp, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface, + ) + } + } + Spacer(Modifier.height(12.dp)) + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + insideMargin = PaddingValues(16.dp), + onClick = { onclickModule() }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Tilt + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.module), + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = getModuleCount().toString(), + fontSize = 26.sp, + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface, ) } } } } + } - systemStatus.kernelVersion.isGKI() -> { - Icon( - Icons.Outlined.Warning, - contentDescription = stringResource(R.string.home_not_installed), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier - .size(28.dp) - .padding( - horizontal = 4.dp - ), + kernelVersion.isGKI() -> { + Card( + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + BasicComponent( + title = stringResource(R.string.home_not_installed), + summary = stringResource(R.string.home_click_to_install), + leftAction = { + Icon( + Icons.Rounded.ErrorOutline, + stringResource(R.string.home_not_installed), + modifier = Modifier + .padding(end = 16.dp), + tint = colorScheme.onBackground, + ) + } ) - - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_not_installed), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_install), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } } + } - else -> { - Icon( - Icons.Outlined.Block, - contentDescription = stringResource(R.string.home_unsupported), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier - .size(28.dp) - .padding( - horizontal = 4.dp - ), + else -> { + Card( + onClick = { + if (kernelVersion.isGKI()) onClickInstall() + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + BasicComponent( + title = stringResource(R.string.home_unsupported), + summary = stringResource(R.string.home_unsupported_reason), + leftAction = { + Icon( + Icons.Rounded.ErrorOutline, + stringResource(R.string.home_unsupported), + modifier = Modifier + .padding(end = 16.dp), + tint = colorScheme.onBackground, + ) + } ) - - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_unsupported), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_unsupported_reason), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } } } } @@ -522,94 +507,57 @@ private fun StatusCard( @Composable fun WarningCard( message: String, - color: Color = MaterialTheme.colorScheme.error, + color: Color = if (isSystemInDarkTheme()) Color(0XFF310808) else Color(0xFFF8E2E2), onClick: (() -> Unit)? = null ) { - ElevatedCard( - colors = getCardColors(color), - elevation = getCardElevation(), + Card( + onClick = { + onClick?.invoke() + }, + colors = CardDefaults.defaultColors( + color = color + ), + showIndication = onClick != null, + pressFeedbackType = PressFeedbackType.Tilt ) { Row( modifier = Modifier .fillMaxWidth() - .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier) - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically + .padding(16.dp) ) { Text( text = message, - style = MaterialTheme.typography.bodyMedium, + color = Color(0xFFF72727), + fontSize = 14.sp ) } } } -@Composable -fun ContributionCard() { - val uriHandler = LocalUriHandler.current - val links = listOf("https://github.com/ShirkNeko", "https://github.com/udochina") - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), - elevation = getCardElevation(), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - val randomIndex = Random.nextInt(links.size) - uriHandler.openUri(links[randomIndex]) - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_ContributionCard_kernelsu), - style = MaterialTheme.typography.titleSmall, - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_ContributionCard_kernelsu), - style = MaterialTheme.typography.bodyMedium, - ) - } - } - } -} - @Composable fun LearnMoreCard() { val uriHandler = LocalUriHandler.current val url = stringResource(R.string.home_learn_kernelsu_url) - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) + Card( + modifier = Modifier + .fillMaxWidth(), ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri(url) - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_learn_kernelsu), - style = MaterialTheme.typography.titleSmall, - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_learn_kernelsu), - style = MaterialTheme.typography.bodyMedium, + BasicComponent( + title = stringResource(R.string.home_learn_kernelsu), + summary = stringResource(R.string.home_click_to_learn_kernelsu), + rightActions = { + Icon( + modifier = Modifier.size(28.dp), + imageVector = Icons.Rounded.Link, + tint = colorScheme.onSurface, + contentDescription = null ) + }, + onClick = { + uriHandler.openUri(url) } - } + ) } } @@ -617,228 +565,76 @@ fun LearnMoreCard() { fun DonateCard() { val uriHandler = LocalUriHandler.current - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), - elevation = getCardElevation(), + Card( + modifier = Modifier + .fillMaxWidth(), ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri("https://patreon.com/weishu") - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_support_title), - style = MaterialTheme.typography.titleSmall, + BasicComponent( + title = stringResource(R.string.home_support_title), + summary = stringResource(R.string.home_support_content), + rightActions = { + Icon( + modifier = Modifier.size(28.dp), + imageVector = Icons.Rounded.Link, + tint = colorScheme.onSurface, + contentDescription = null ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_support_content), - style = MaterialTheme.typography.bodyMedium, - ) - } - } + }, + onClick = { + uriHandler.openUri("https://patreon.com/weishu") + }, + insideMargin = PaddingValues(18.dp) + ) } } @Composable -private fun InfoCard( - systemInfo: HomeViewModel.SystemInfo, - isSimpleMode: Boolean, - isHideSusfsStatus: Boolean, - isHideZygiskImplement: Boolean, - showKpmInfo: Boolean, - lkmMode: Boolean? -) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), - elevation = getCardElevation(), +private fun InfoCard() { + @Composable + fun InfoText( + title: String, + content: String, + bottomPadding: Dp = 24.dp ) { + Text( + text = title, + fontSize = MiuixTheme.textStyles.headline1.fontSize, + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + Text( + text = content, + fontSize = MiuixTheme.textStyles.body2.fontSize, + color = colorScheme.onSurfaceVariantSummary, + modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding) + ) + } + Card { + val context = LocalContext.current + val uname = Os.uname() + val managerVersion = getManagerVersion(context) Column( modifier = Modifier .fillMaxWidth() - .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp), + .padding(16.dp) ) { - @Composable - fun InfoCardItem( - label: String, - content: String, - icon: ImageVector? = null, - ) { - Row( - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = label, - modifier = Modifier - .size(28.dp) - .padding(vertical = 4.dp), - ) - } - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Text( - text = label, - style = MaterialTheme.typography.labelLarge, - ) - Text( - text = content, - style = MaterialTheme.typography.bodyMedium, - softWrap = true - ) - } - } - } - - InfoCardItem( - stringResource(R.string.home_kernel), - systemInfo.kernelRelease, - icon = Icons.Default.Memory, + InfoText( + title = stringResource(R.string.home_kernel), + content = uname.release ) - - if (!isSimpleMode) { - InfoCardItem( - stringResource(R.string.home_android_version), - systemInfo.androidVersion, - icon = Icons.Default.Android, - ) - } - - InfoCardItem( - stringResource(R.string.home_device_model), - systemInfo.deviceModel, - icon = Icons.Default.PhoneAndroid, + InfoText( + title = stringResource(R.string.home_manager_version), + content = "${managerVersion.first} (${managerVersion.second})" ) - - InfoCardItem( - stringResource(R.string.home_manager_version), - "${systemInfo.managerVersion.first} (${systemInfo.managerVersion.second.toInt()})", - icon = Icons.Default.SettingsSuggest, + InfoText( + title = stringResource(R.string.home_fingerprint), + content = Build.FINGERPRINT ) - - if (!isSimpleMode && - (systemInfo.suSFSStatus != "Supported")) { - InfoCardItem( - stringResource(R.string.home_hook_type), - Natives.getHookType(), - icon = Icons.Default.Link - ) - } - - // 活跃管理器 - if (!isSimpleMode && systemInfo.isDynamicSignEnabled && systemInfo.managersList != null) { - val signatureMap = systemInfo.managersList.managers.groupBy { it.signatureIndex } - - val managersText = buildString { - signatureMap.toSortedMap().forEach { (signatureIndex, managers) -> - append(managers.joinToString(", ") { "UID: ${it.uid}" }) - append(" ") - append( - when (signatureIndex) { - 0 -> "(${stringResource(R.string.default_signature)})" - 100 -> "(${stringResource(R.string.dynamic_managerature)})" - else -> if (signatureIndex >= 1) "(${ - stringResource( - R.string.signature_index, - signatureIndex - ) - })" else "(${stringResource(R.string.unknown_signature)})" - } - ) - append(" | ") - } - }.trimEnd(' ', '|') - - InfoCardItem( - stringResource(R.string.multi_manager_list), - managersText.ifEmpty { stringResource(R.string.no_active_manager) }, - icon = Icons.Default.Group, - ) - } - - InfoCardItem( - stringResource(R.string.home_selinux_status), - systemInfo.seLinuxStatus, - icon = Icons.Default.Security, + InfoText( + title = stringResource(R.string.home_selinux_status), + content = getSELinuxStatus(), + bottomPadding = 0.dp ) - - if (!isHideZygiskImplement && !isSimpleMode && systemInfo.zygiskImplement != "None") { - InfoCardItem( - stringResource(R.string.home_zygisk_implement), - systemInfo.zygiskImplement, - icon = Icons.Default.Adb, - ) - } - - if (!isSimpleMode) { - if (lkmMode != true && !showKpmInfo) { - val displayVersion = - if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) { - val statusText = if (Natives.isKPMEnabled()) { - stringResource(R.string.kernel_patched) - } else { - stringResource(R.string.kernel_not_enabled) - } - "${stringResource(R.string.not_supported)} ($statusText)" - } else { - "${stringResource(R.string.supported)} (${systemInfo.kpmVersion})" - } - - InfoCardItem( - stringResource(R.string.home_kpm_version), - displayVersion, - icon = Icons.Default.Archive - ) - } - } - - if (!isSimpleMode && !isHideSusfsStatus && - systemInfo.suSFSStatus == "Supported" && - systemInfo.suSFSVersion.isNotEmpty() - ) { - - val infoText = SuSFSInfoText(systemInfo) - - InfoCardItem( - stringResource(R.string.home_susfs_version), - infoText, - icon = Icons.Default.Storage - ) - } - } - } -} - -@SuppressLint("ComposableNaming") -@Composable -private fun SuSFSInfoText(systemInfo: HomeViewModel.SystemInfo): String = buildString { - append(systemInfo.suSFSVersion) - - when { - Natives.getHookType() == "Manual" -> { - append(" (${stringResource(R.string.manual_hook)})") - } - - Natives.getHookType() == "Inline" -> { - append(" (${stringResource(R.string.inline_hook)})") - } - - else -> { - append(" (${Natives.getHookType()})") } } } @@ -848,78 +644,3 @@ fun getManagerVersion(context: Context): Pair { val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) return Pair(packageInfo.versionName!!, versionCode) } - -@Preview -@Composable -private fun StatusCardPreview() { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - StatusCard( - HomeViewModel.SystemStatus( - isManager = true, - ksuVersion = 1, - lkmMode = null, - kernelVersion = KernelVersion(5, 10, 101), - isRootAvailable = true - ) - ) - - StatusCard( - HomeViewModel.SystemStatus( - isManager = true, - ksuVersion = 40000, - lkmMode = true, - kernelVersion = KernelVersion(5, 10, 101), - isRootAvailable = true - ) - ) - - StatusCard( - HomeViewModel.SystemStatus( - isManager = false, - ksuVersion = null, - lkmMode = true, - kernelVersion = KernelVersion(5, 10, 101), - isRootAvailable = false - ) - ) - - StatusCard( - HomeViewModel.SystemStatus( - isManager = false, - ksuVersion = null, - lkmMode = false, - kernelVersion = KernelVersion(4, 10, 101), - isRootAvailable = false - ) - ) - } -} - -@Composable -private fun IncompatibleKernelCard() { - val currentKver = remember { Natives.version } - val threshold = Natives.MINIMAL_NEW_IOCTL_KERNEL - - val msg = stringResource( - id = R.string.incompatible_kernel_msg, - currentKver, - threshold - ) - - WarningCard( - message = msg, - color = MaterialTheme.colorScheme.error - ) -} - -@Preview -@Composable -private fun WarningCardPreview() { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - WarningCard(message = "Warning message") - WarningCard( - message = "Warning message ", - MaterialTheme.colorScheme.outlineVariant, - onClick = {}) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 5b04b89d..b0f26561 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -9,134 +9,101 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +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.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Input -import androidx.compose.material.icons.filled.AutoFixHigh -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.FileUpload -import androidx.compose.material.icons.filled.Security -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.toggleable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -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 androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination -import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import com.sukisu.ultra.R -import com.sukisu.ultra.getKernelVersion -import com.sukisu.ultra.ui.component.DialogHandle +import com.sukisu.ultra.ui.component.ChooseKmiDialog import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.component.rememberCustomDialog -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.* -import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog +import com.sukisu.ultra.ui.util.LkmSelection +import com.sukisu.ultra.ui.util.getAvailablePartitions +import com.sukisu.ultra.ui.util.getCurrentKmi +import com.sukisu.ultra.ui.util.getDefaultPartition +import com.sukisu.ultra.ui.util.getSlotSuffix +import com.sukisu.ultra.ui.util.isAbDevice +import com.sukisu.ultra.ui.util.rootAvailable +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperCheckbox +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Edit +import top.yukonga.miuix.kmp.icon.icons.useful.Move +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** - * @author ShirkNeko - * @date 2025/5/31. + * @author weishu + * @date 2024/3/12. */ - -enum class KpmPatchOption { - FOLLOW_KERNEL, - PATCH_KPM, - UNDO_PATCH_KPM -} - -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable -fun InstallScreen( - navigator: DestinationsNavigator, - preselectedKernelUri: String? = null -) { +@Destination +fun InstallScreen(navigator: DestinationsNavigator) { val context = LocalContext.current - var installMethod by remember { mutableStateOf(null) } - var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } - var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) } - var showRebootDialog by remember { mutableStateOf(false) } - var showSlotSelectionDialog by remember { mutableStateOf(false) } - var showKpmPatchDialog by remember { mutableStateOf(false) } - var tempKernelUri by remember { mutableStateOf(null) } - - val kernelVersion = getKernelVersion() - val isGKI = kernelVersion.isGKI() - val isAbDevice = produceState(initialValue = false) { - value = isAbDevice() - }.value - val summary = stringResource(R.string.horizon_kernel_summary) - - // 处理预选的内核文件 - LaunchedEffect(preselectedKernelUri) { - preselectedKernelUri?.let { uriString -> - try { - val preselectedUri = uriString.toUri() - val horizonMethod = InstallMethod.HorizonKernel( - uri = preselectedUri, - summary = summary - ) - installMethod = horizonMethod - tempKernelUri = preselectedUri - - if (isAbDevice) { - showSlotSelectionDialog = true - } else { - showKpmPatchDialog = true - } - } catch (e: Exception) { - e.printStackTrace() - } - } + var installMethod by remember { + mutableStateOf(null) } - if (showRebootDialog) { - RebootDialog( - show = true, - onDismiss = { showRebootDialog = false }, - onConfirm = { - showRebootDialog = false - try { - val process = Runtime.getRuntime().exec("su") - process.outputStream.bufferedWriter().use { writer -> - writer.write("svc power reboot\n") - writer.write("exit\n") - } - } catch (_: Exception) { - Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show() - } - } - ) + var lkmSelection by remember { + mutableStateOf(LkmSelection.KmiNone) } var partitionSelectionIndex by remember { mutableIntStateOf(0) } @@ -145,69 +112,24 @@ fun InstallScreen( val onInstall = { installMethod?.let { method -> - when (method) { - is InstallMethod.HorizonKernel -> { - method.uri?.let { uri -> - navigator.navigate( - KernelFlashScreenDestination( - kernelUri = uri, - selectedSlot = method.slot, - kpmPatchEnabled = kpmPatchOption == KpmPatchOption.PATCH_KPM, - kpmUndoPatch = kpmPatchOption == KpmPatchOption.UNDO_PATCH_KPM - ) - ) - } - } - else -> { - val isOta = method is InstallMethod.DirectInstallToInactiveSlot - val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex) - val flashIt = FlashIt.FlashBoot( - boot = if (method is InstallMethod.SelectFile) method.uri else null, - lkm = lkmSelection, - ota = isOta, - partition = partitionSelection - ) - navigator.navigate(FlashScreenDestination(flashIt)) - } - } - } - Unit - } - - // 槽位选择 - SlotSelectionDialog( - show = showSlotSelectionDialog && isAbDevice, - onDismiss = { showSlotSelectionDialog = false }, - onSlotSelected = { slot -> - showSlotSelectionDialog = false - val horizonMethod = InstallMethod.HorizonKernel( - uri = tempKernelUri, - slot = slot, - summary = summary + val isOta = method is InstallMethod.DirectInstallToInactiveSlot + val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex) + val flashIt = FlashIt.FlashBoot( + boot = if (method is InstallMethod.SelectFile) method.uri else null, + lkm = lkmSelection, + ota = isOta, + partition = partitionSelection ) - installMethod = horizonMethod - - if (preselectedKernelUri != null) { - showKpmPatchDialog = true + navigator.navigate(FlashScreenDestination(flashIt)) { + launchSingleTop = true } } - ) - - KpmPatchSelectionDialog( - show = showKpmPatchDialog, - currentOption = kpmPatchOption, - onDismiss = { showKpmPatchDialog = false }, - onOptionSelected = { option -> - kpmPatchOption = option - showKpmPatchDialog = false - } - ) - - val currentKmi by produceState(initialValue = "") { - value = getCurrentKmi() } - val selectKmiDialog = rememberSelectKmiDialog { kmi -> + val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } + + val showChooseKmiDialog = rememberSaveable { mutableStateOf(false) } + val chooseKmiDialog = ChooseKmiDialog(showChooseKmiDialog) { kmi -> kmi?.let { lkmSelection = LkmSelection.KmiString(it) onInstall() @@ -215,32 +137,33 @@ fun InstallScreen( } val onClickNext = { - if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank() && installMethod !is InstallMethod.HorizonKernel) { - selectKmiDialog.show() + if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { + // no lkm file selected and cannot get current kmi + showChooseKmiDialog.value = true + chooseKmiDialog } else { onInstall() } } - val selectLkmLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { uri -> - val isKo = isKoFile(context, uri) - if (isKo) { - lkmSelection = LkmSelection.LkmUri(uri) - } else { - lkmSelection = LkmSelection.KmiNone - Toast.makeText( - context, - context.getString(R.string.install_only_support_ko_file), - Toast.LENGTH_SHORT - ).show() + val selectLkmLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> + val isKo = isKoFile(context, uri) + if (isKo) { + lkmSelection = LkmSelection.LkmUri(uri) + } else { + lkmSelection = LkmSelection.KmiNone + Toast.makeText( + context, + context.getString(R.string.install_only_support_ko_file), + Toast.LENGTH_SHORT + ).show() + } } } } - } val onLkmUpload = { selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { @@ -248,85 +171,72 @@ fun InstallScreen( }) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = MiuixTheme.colorScheme.background, + tint = HazeTint(MiuixTheme.colorScheme.background.copy(0.8f)) + ) Scaffold( topBar = { TopBar( - onBack = { navigator.popBackStack() }, - scrollBehavior = scrollBehavior + onBack = dropUnlessResumed { navigator.popBackStack() }, + scrollBehavior = scrollBehavior, + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier - .padding(innerPadding) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) + .hazeSource(state = hazeState) .padding(top = 12.dp) + .padding(horizontal = 16.dp), + contentPadding = innerPadding, + overscrollEffect = null, ) { - SelectInstallMethod( - isGKI = isGKI, - onSelected = { method -> - if (method is InstallMethod.HorizonKernel && method.uri != null) { - if (isAbDevice) { - tempKernelUri = method.uri - showSlotSelectionDialog = true - } else { - installMethod = method - showKpmPatchDialog = true - } - } else { + item { + Card( + modifier = Modifier + .fillMaxWidth(), + ) { + SelectInstallMethod { method -> installMethod = method } - }, - kpmPatchOption = kpmPatchOption, - onKpmPatchOptionChanged = { kpmPatchOption = it }, - selectedMethod = installMethod - ) - - // 选择LKM直接安装分区 - AnimatedVisibility( - visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + } + AnimatedVisibility( + visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, + enter = expandVertically(), + exit = shrinkVertically() ) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = getCardElevation(), + Card( modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp), + .padding(top = 12.dp), ) { val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot val suffix = produceState(initialValue = "", isOta) { value = getSlotSuffix(isOta) }.value - val partitions = produceState(initialValue = emptyList()) { value = getAvailablePartitions() }.value - val defaultPartition = produceState(initialValue = "") { value = getDefaultPartition() }.value - partitionsState = partitions val displayPartitions = partitions.map { name -> if (defaultPartition == name) "$name (default)" else name } - val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0 if (!hasCustomSelected) partitionSelectionIndex = defaultIndex - SuperDropdown( items = displayPartitions, selectedIndex = partitionSelectionIndex, @@ -337,8 +247,8 @@ fun InstallScreen( }, leftAction = { Icon( - Icons.Default.Edit, - tint = MaterialTheme.colorScheme.onSurface, + MiuixIcons.Useful.Edit, + tint = colorScheme.onSurface, modifier = Modifier.padding(end = 16.dp), contentDescription = null ) @@ -346,189 +256,59 @@ fun InstallScreen( ) } } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - if (isGKI) { - // 使用本地的LKM文件 - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - ) { - ListItem( - headlineContent = { - Text(stringResource(id = R.string.install_upload_lkm_file)) - }, - supportingContent = { - (lkmSelection as? LkmSelection.LkmUri)?.let { - Text( - stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" - ) - ) - } - }, - leadingContent = { - Icon( - Icons.AutoMirrored.Filled.Input, - contentDescription = null - ) - }, - modifier = Modifier - .fillMaxWidth() - .clickable { onLkmUpload() } - ) - } - } - - (installMethod as? InstallMethod.HorizonKernel)?.let { method -> - if (method.slot != null) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - ) { - 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) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + ) { + SuperArrow( + title = stringResource(id = R.string.install_upload_lkm_file), + summary = (lkmSelection as? LkmSelection.LkmUri)?.let { + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + }, + onClick = onLkmUpload, + leftAction = { + Icon( + MiuixIcons.Useful.Move, + tint = colorScheme.onSurface, + modifier = Modifier.padding(end = 16.dp), + contentDescription = null ) } - } - - // KPM 状态显示卡片 - if (kpmPatchOption != KpmPatchOption.FOLLOW_KERNEL) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - ) { - Text( - text = when (kpmPatchOption) { - KpmPatchOption.PATCH_KPM -> stringResource(R.string.kpm_patch_enabled) - KpmPatchOption.UNDO_PATCH_KPM -> stringResource(R.string.kpm_undo_patch_enabled) - else -> "" - }, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp), - color = when (kpmPatchOption) { - KpmPatchOption.PATCH_KPM -> MaterialTheme.colorScheme.primary - KpmPatchOption.UNDO_PATCH_KPM -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) - } - } - } - - Button( - modifier = Modifier.fillMaxWidth(), - enabled = installMethod != null, - 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) ) + } + Button( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + enabled = installMethod != null, + colors = ButtonDefaults.buttonColorsPrimary(), + onClick = { onClickNext() } ) { Text( stringResource(id = R.string.install_next), - style = MaterialTheme.typography.bodyMedium + color = colorScheme.onPrimary, + fontSize = MiuixTheme.textStyles.body1.fontSize ) } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) } } } } -@Composable -private fun KpmPatchSelectionDialog( - show: Boolean, - currentOption: KpmPatchOption, - onDismiss: () -> Unit, - onOptionSelected: (KpmPatchOption) -> Unit -) { - if (show) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.kpm_patch_options)) }, - text = { - Column { - Text( - text = stringResource(R.string.kpm_patch_description), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) - ) - - KpmPatchOptionGroup( - selectedOption = currentOption, - onOptionChanged = onOptionSelected - ) - } - }, - confirmButton = { - TextButton( - onClick = { onOptionSelected(currentOption) } - ) { - Text(stringResource(android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(android.R.string.cancel)) - } - } - ) - } -} - -@Composable -private fun RebootDialog( - show: Boolean, - onDismiss: () -> Unit, - onConfirm: () -> Unit -) { - if (show) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(id = R.string.reboot_complete_title)) }, - text = { Text(stringResource(id = R.string.reboot_complete_msg)) }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(stringResource(id = R.string.yes)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(id = R.string.no)) - } - } - ) - } -} - sealed class InstallMethod { data class SelectFile( val uri: Uri? = null, - @param:StringRes override val label: Int = R.string.select_file, + @get:StringRes override val label: Int = R.string.select_file, override val summary: String? ) : InstallMethod() @@ -542,25 +322,12 @@ sealed class InstallMethod { get() = R.string.install_inactive_slot } - data class HorizonKernel( - val uri: Uri? = null, - val slot: String? = null, - @param:StringRes override val label: Int = R.string.horizon_kernel, - override val summary: String? = null - ) : InstallMethod() - abstract val label: Int open val summary: String? = null } @Composable -private fun SelectInstallMethod( - isGKI: Boolean = false, - onSelected: (InstallMethod) -> Unit = {}, - kpmPatchOption: KpmPatchOption = KpmPatchOption.FOLLOW_KERNEL, - onKpmPatchOptionChanged: (KpmPatchOption) -> Unit = {}, - selectedMethod: InstallMethod? = null -) { +private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { val rootAvailable = rootAvailable() val isAbDevice = produceState(initialValue = false) { value = isAbDevice() @@ -568,52 +335,27 @@ private fun SelectInstallMethod( val defaultPartitionName = produceState(initialValue = "boot") { value = getDefaultPartition() }.value - val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) val selectFileTip = stringResource( id = R.string.select_file_tip, defaultPartitionName ) - - val radioOptions = mutableListOf( - InstallMethod.SelectFile(summary = selectFileTip) - ) - + val radioOptions = mutableListOf(InstallMethod.SelectFile(summary = selectFileTip)) if (rootAvailable) { radioOptions.add(InstallMethod.DirectInstall) + if (isAbDevice) { radioOptions.add(InstallMethod.DirectInstallToInactiveSlot) } - radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary)) } var selectedOption by remember { mutableStateOf(null) } - var currentSelectingMethod by remember { mutableStateOf(null) } - - LaunchedEffect(selectedMethod) { - selectedOption = selectedMethod - } - val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { it.data?.data?.let { uri -> - val option = when (currentSelectingMethod) { - is InstallMethod.SelectFile -> InstallMethod.SelectFile( - uri, - summary = selectFileTip - ) - - is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel( - uri, - summary = horizonKernelSummary - ) - - else -> null - } - option?.let { opt -> - selectedOption = opt - onSelected(opt) - } + val option = InstallMethod.SelectFile(uri, summary = selectFileTip) + selectedOption = option + onSelected(option) } } } @@ -622,23 +364,17 @@ private fun SelectInstallMethod( onConfirm = { selectedOption = InstallMethod.DirectInstallToInactiveSlot onSelected(InstallMethod.DirectInstallToInactiveSlot) - }, - onDismiss = null + } ) - val dialogTitle = stringResource(id = android.R.string.dialog_alert_title) val dialogContent = stringResource(id = R.string.install_inactive_slot_warning) val onClick = { option: InstallMethod -> - currentSelectingMethod = option + when (option) { - is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> { + is InstallMethod.SelectFile -> { selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/*" - putExtra( - Intent.EXTRA_MIME_TYPES, - arrayOf("application/octet-stream", "application/zip") - ) + type = "application/octet-stream" }) } @@ -653,419 +389,63 @@ private fun SelectInstallMethod( } } - 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 = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant - ) - ) { - 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.filter { it !is 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() - .selectable( - selected = option.javaClass == selectedOption?.javaClass, - onClick = { 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 = getCardElevation(), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - ) { - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant - ) - ) { - 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().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() - .selectable( - selected = option.javaClass == selectedOption?.javaClass, - onClick = { 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 - ) - } - } - } - } - } - - // KPM修补 - if (selectedMethod is InstallMethod.HorizonKernel && selectedMethod.uri != null) { - Spacer(modifier = Modifier.height(16.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Icon( - Icons.Filled.Security, - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - stringResource(R.string.kpm_patch_options), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.tertiary - ) - } - - Text( - stringResource(R.string.kpm_patch_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 12.dp) - ) - - KpmPatchOptionGroup( - selectedOption = kpmPatchOption, - onOptionChanged = onKpmPatchOptionChanged - ) - } - } - } - } - } - } -} - -@Composable -private fun KpmPatchOptionGroup( - selectedOption: KpmPatchOption, - onOptionChanged: (KpmPatchOption) -> Unit -) { - val options = listOf( - KpmPatchOption.FOLLOW_KERNEL to stringResource(R.string.kpm_follow_kernel_file), - KpmPatchOption.PATCH_KPM to stringResource(R.string.enable_kpm_patch), - KpmPatchOption.UNDO_PATCH_KPM to stringResource(R.string.enable_kpm_undo_patch) - ) - - val descriptions = mapOf( - KpmPatchOption.FOLLOW_KERNEL to stringResource(R.string.kpm_follow_kernel_description), - KpmPatchOption.PATCH_KPM to stringResource(R.string.kpm_patch_switch_description), - KpmPatchOption.UNDO_PATCH_KPM to stringResource(R.string.kpm_undo_patch_switch_description) - ) - Column { - options.forEach { (option, label) -> + radioOptions.forEach { option -> val interactionSource = remember { MutableInteractionSource() } - Surface( - color = if (option == selectedOption) - MaterialTheme.colorScheme.primaryContainer.copy(alpha = cardAlpha) - else - MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardAlpha), - shape = MaterialTheme.shapes.medium, + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(vertical = 2.dp) - .clip(MaterialTheme.shapes.medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = option == selectedOption, - onClick = { onOptionChanged(option) }, - role = Role.RadioButton, - indication = LocalIndication.current, - interactionSource = interactionSource - ) - .padding(vertical = 12.dp, horizontal = 12.dp) - ) { - RadioButton( - selected = option == selectedOption, - onClick = null, - interactionSource = interactionSource, - colors = RadioButtonDefaults.colors( - selectedColor = when (option) { - KpmPatchOption.FOLLOW_KERNEL -> MaterialTheme.colorScheme.primary - KpmPatchOption.PATCH_KPM -> MaterialTheme.colorScheme.primary - KpmPatchOption.UNDO_PATCH_KPM -> MaterialTheme.colorScheme.tertiary - }, - unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant - ) + .toggleable( + value = option.javaClass == selectedOption?.javaClass, + onValueChange = { + onClick(option) + }, + role = Role.RadioButton, + indication = LocalIndication.current, + interactionSource = interactionSource ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - color = if (option == selectedOption) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurface - ) - descriptions[option]?.let { description -> - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = if (option == selectedOption) - MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - else - MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 2.dp) - ) - } - } - } + ) { + SuperCheckbox( + title = stringResource(id = option.label), + summary = option.summary, + checked = option.javaClass == selectedOption?.javaClass, + onCheckedChange = { + onClick(option) + }, + ) } } } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val supportedKmi by produceState(initialValue = emptyList()) { - value = getSupportedKmis() - } - val options = supportedKmi.map { value -> - ListOption( - titleText = value - ) - } - - var selection by remember { mutableStateOf(null) } - - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { - onSelected(selection) - }, onCloseRequest = { - dismiss() - }), header = Header.Default( - title = stringResource(R.string.select_kmi), - ), selection = ListSelection.Single( - showRadioButtons = true, - options = options, - ) { _, option -> - selection = option.titleText - }) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, + hazeState: HazeState, + hazeStyle: HazeStyle, ) { - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - val cardAlpha = cardAlpha - TopAppBar( - title = { - Text( - stringResource(R.string.install), - style = MaterialTheme.typography.titleLarge - ) + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), + color = Color.Transparent, + title = stringResource(R.string.install), navigationIcon = { - IconButton(onClick = onBack) { + IconButton( + modifier = Modifier.padding(start = 16.dp), + onClick = onBack + ) { Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) + MiuixIcons.Useful.Back, + tint = colorScheme.onSurface, + contentDescription = null, ) } }, - windowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), scrollBehavior = scrollBehavior ) } @@ -1094,9 +474,3 @@ private fun isKoFile(context: Context, uri: Uri): Boolean { false } } - -@Preview -@Composable -fun SelectInstallPreview() { - InstallScreen(EmptyDestinationsNavigator) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt deleted file mode 100644 index 6f36c672..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt +++ /dev/null @@ -1,742 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.Context -import android.content.Intent -import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import com.sukisu.ultra.ui.component.* -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.viewmodel.KpmViewModel -import com.sukisu.ultra.ui.util.* -import java.io.File -import androidx.core.content.edit -import com.sukisu.ultra.R -import java.io.FileInputStream -import java.net.* -import android.app.Activity -import androidx.compose.ui.res.painterResource - -/** - * KPM 管理界面 - * 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能 - * 开发者:ShirkNeko, Liaokong - */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun KpmScreen( - viewModel: KpmViewModel = viewModel() -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val snackBarHost = remember { SnackbarHostState() } - val confirmDialog = rememberConfirmDialog() - - val listState = rememberLazyListState() - val fabVisible by rememberFabVisibilityState(listState) - - val moduleConfirmContentMap = viewModel.moduleList.associate { module -> - val moduleFileName = module.id - module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName) - } - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - val kpmInstallSuccess = stringResource(R.string.kpm_install_success) - val kpmInstallFailed = stringResource(R.string.kpm_install_failed) - val cancel = stringResource(R.string.cancel) - val uninstall = stringResource(R.string.uninstall) - val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file) - val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success) - val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed) - val kpmInstallMode = stringResource(R.string.kpm_install_mode) - val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load) - val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed) - val invalidFileTypeMessage = stringResource(R.string.invalid_file_type) - val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename) - - var tempFileForInstall by remember { mutableStateOf(null) } - val installModeDialog = rememberCustomDialog { dismiss -> - var moduleName by remember { mutableStateOf(null) } - - LaunchedEffect(tempFileForInstall) { - tempFileForInstall?.let { tempFile -> - try { - val shell = getRootShell() - val command = "strings ${tempFile.absolutePath} | grep 'name='" - val result = shell.newJob().add(command).to(ArrayList(), null).exec() - if (result.isSuccess) { - for (line in result.out) { - if (line.startsWith("name=")) { - moduleName = line.substringAfter("name=").trim() - break - } - } - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module name: ${e.message}", e) - } - } - } - - AlertDialog( - onDismissRequest = { - dismiss() - tempFileForInstall?.delete() - tempFileForInstall = null - }, - title = { - Text( - text = kpmInstallMode, - style = MaterialTheme.typography.headlineSmall, - ) - }, - text = { - Column { - moduleName?.let { - Text( - text = stringResource(R.string.kpm_install_mode_description, it), - style = MaterialTheme.typography.bodyMedium, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { - scope.launch { - dismiss() - tempFileForInstall?.let { tempFile -> - handleModuleInstall( - tempFile = tempFile, - isEmbed = false, - viewModel = viewModel, - snackBarHost = snackBarHost, - kpmInstallSuccess = kpmInstallSuccess, - kpmInstallFailed = kpmInstallFailed - ) - } - tempFileForInstall = null - } - }, - modifier = Modifier.fillMaxWidth(), - ) { - 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(), - ) { - Icon( - imageVector = Icons.Filled.Inventory, - contentDescription = null, - modifier = Modifier.size(18.dp).padding(end = 4.dp) - ) - Text(kpmInstallModeEmbed) - } - } - } - }, - confirmButton = { - }, - dismissButton = { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { - dismiss() - tempFileForInstall?.delete() - tempFileForInstall = null - } - ) { - Text(cancel) - } - } - }, - shape = MaterialTheme.shapes.extraLarge - ) - } - - val selectPatchLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - - scope.launch { - val fileName = uri.lastPathSegment ?: "unknown.kpm" - val encodedFileName = URLEncoder.encode(fileName, "UTF-8") - val tempFile = File(context.cacheDir, encodedFileName) - - context.contentResolver.openInputStream(uri)?.use { input -> - tempFile.outputStream().use { output -> - input.copyTo(output) - } - } - - val mimeType = context.contentResolver.getType(uri) - val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream") - - if (!isCorrectMimeType) { - var shouldShowSnackbar = true - try { - val matchCount = checkStringsCommand(tempFile) - val isElf = isElfFile(tempFile) - - if (matchCount >= 1 || isElf) { - shouldShowSnackbar = false - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to execute checks: ${e.message}", e) - } - if (shouldShowSnackbar) { - snackBarHost.showSnackbar( - message = invalidFileTypeMessage, - duration = SnackbarDuration.Short - ) - } - tempFile.delete() - return@launch - } - tempFileForInstall = tempFile - installModeDialog.show() - } - } - - LaunchedEffect(Unit) { - while(true) { - viewModel.fetchModuleList() - delay(5000) - } - } - - val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) - var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) } - - Scaffold( - topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.kpm_title)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - scrollBehavior = scrollBehavior, - dropdownContent = { - IconButton( - onClick = { viewModel.fetchModuleList() } - ) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(R.string.refresh), - ) - } - } - ) - }, - floatingActionButton = { - AnimatedFab(visible = fabVisible) { - FloatingActionButton( - contentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = MaterialTheme.colorScheme.primary, - onClick = { - selectPatchLauncher.launch( - Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/octet-stream" - } - ) - }, - content = { - Icon( - painter = painterResource(id = R.drawable.package_import), - contentDescription = null - ) - } - ) - } - }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), - snackbarHost = { SnackbarHost(snackBarHost) } - ) { padding -> - Column(modifier = Modifier.padding(padding)) { - if (!isNoticeClosed) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(MaterialTheme.shapes.medium) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Filled.Info, - contentDescription = null, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Text( - text = stringResource(R.string.kernel_module_notice), - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, - ) - - IconButton( - onClick = { - isNoticeClosed = true - sharedPreferences.edit { putBoolean("is_notice_closed", true) } - }, - modifier = Modifier.size(24.dp), - ) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = stringResource(R.string.close_notice) - ) - } - } - } - } - - if (viewModel.moduleList.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Code, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - stringResource(R.string.kpm_empty), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - } - } - } else { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(viewModel.moduleList) { module -> - KpmModuleItem( - module = module, - onUninstall = { - scope.launch { - val confirmContent = moduleConfirmContentMap[module.id] ?: "" - handleModuleUninstall( - module = module, - viewModel = viewModel, - snackBarHost = snackBarHost, - kpmUninstallSuccess = kpmUninstallSuccess, - kpmUninstallFailed = kpmUninstallFailed, - failedToCheckModuleFile = failedToCheckModuleFile, - uninstall = uninstall, - cancel = cancel, - confirmDialog = confirmDialog, - confirmTitle = confirmTitle, - confirmContent = confirmContent - ) - } - }, - onControl = { - viewModel.loadModuleDetail(module.id) - } - ) - } - } - } - } - } -} - -private suspend fun handleModuleInstall( - tempFile: File, - isEmbed: Boolean, - viewModel: KpmViewModel, - snackBarHost: SnackbarHostState, - kpmInstallSuccess: String, - kpmInstallFailed: String -) { - var moduleId: String? = null - try { - val shell = getRootShell() - val command = "strings ${tempFile.absolutePath} | grep 'name='" - val result = shell.newJob().add(command).to(ArrayList(), null).exec() - if (result.isSuccess) { - for (line in result.out) { - if (line.startsWith("name=")) { - moduleId = line.substringAfter("name=").trim() - break - } - } - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e) - } - - if (moduleId == null || moduleId.isEmpty()) { - Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}") - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - tempFile.delete() - return - } - - val targetPath = "/data/adb/kpm/$moduleId.kpm" - - try { - if (isEmbed) { - val shell = getRootShell() - shell.newJob().add("mkdir -p /data/adb/kpm").exec() - shell.newJob().add("cp ${tempFile.absolutePath} $targetPath").exec() - } - - val loadResult = loadKpmModule(tempFile.absolutePath) - if (loadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to load KPM module: $loadResult") - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - } else { - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmInstallSuccess, - duration = SnackbarDuration.Short - ) - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e) - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - } - tempFile.delete() -} - -private suspend fun handleModuleUninstall( - module: KpmViewModel.ModuleInfo, - viewModel: KpmViewModel, - snackBarHost: SnackbarHostState, - kpmUninstallSuccess: String, - kpmUninstallFailed: String, - failedToCheckModuleFile: String, - uninstall: String, - cancel: String, - confirmTitle : String, - confirmContent : String, - confirmDialog: ConfirmDialogHandle -) { - val moduleFileName = "${module.id}.kpm" - val moduleFilePath = "/data/adb/kpm/$moduleFileName" - - val fileExists = try { - val shell = getRootShell() - val result = shell.newJob().add("ls /data/adb/kpm/$moduleFileName").exec() - result.isSuccess - } catch (e: Exception) { - Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e) - snackBarHost.showSnackbar( - message = failedToCheckModuleFile, - duration = SnackbarDuration.Short - ) - false - } - - val confirmResult = confirmDialog.awaitConfirm( - title = confirmTitle, - content = confirmContent, - confirm = uninstall, - dismiss = cancel - ) - - if (confirmResult == ConfirmResult.Confirmed) { - try { - val unloadResult = unloadKpmModule(module.id) - if (unloadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to unload KPM module: $unloadResult") - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Short - ) - return - } - - if (fileExists) { - val shell = getRootShell() - shell.newJob().add("rm $moduleFilePath").exec() - } - - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmUninstallSuccess, - duration = SnackbarDuration.Short - ) - } catch (e: Exception) { - Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e) - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Short - ) - } - } -} - -@Composable -private fun KpmModuleItem( - module: KpmViewModel.ModuleInfo, - onUninstall: () -> Unit, - onControl: () -> Unit -) { - val viewModel: KpmViewModel = viewModel() - val scope = rememberCoroutineScope() - val snackBarHost = remember { SnackbarHostState() } - val successMessage = stringResource(R.string.kpm_control_success) - val failureMessage = stringResource(R.string.kpm_control_failed) - - if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) { - AlertDialog( - onDismissRequest = { viewModel.hideInputDialog() }, - title = { - Text( - text = stringResource(R.string.kpm_control), - style = MaterialTheme.typography.headlineSmall, - ) - }, - text = { - OutlinedTextField( - value = viewModel.inputArgs, - onValueChange = { viewModel.updateInputArgs(it) }, - label = { - Text( - text = stringResource(R.string.kpm_args), - ) - }, - placeholder = { - Text( - text = module.args, - ) - }, - modifier = Modifier.fillMaxWidth(), - ) - }, - confirmButton = { - TextButton( - onClick = { - scope.launch { - val result = viewModel.executeControl() - val message = when (result) { - 0 -> successMessage - else -> failureMessage - } - snackBarHost.showSnackbar(message) - onControl() - } - } - ) { - Text( - text = stringResource(R.string.confirm), - ) - } - }, - dismissButton = { - TextButton(onClick = { viewModel.hideInputDialog() }) { - Text( - text = stringResource(R.string.cancel), - ) - } - }, - shape = MaterialTheme.shapes.extraLarge - ) - } - - Card( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = getCardElevation() - ) { - Column( - modifier = Modifier.padding(20.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = module.name, - style = MaterialTheme.typography.titleLarge, - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "${stringResource(R.string.kpm_version)}: ${module.version}", - style = MaterialTheme.typography.bodyMedium, - ) - - Text( - text = "${stringResource(R.string.kpm_author)}: ${module.author}", - style = MaterialTheme.typography.bodyMedium, - ) - - Text( - text = "${stringResource(R.string.kpm_args)}: ${module.args}", - style = MaterialTheme.typography.bodyMedium, - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = module.description, - style = MaterialTheme.typography.bodyLarge, - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { viewModel.showInputDialog(module.id) }, - enabled = module.hasAction, - modifier = Modifier.weight(1f), - ) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.kpm_control)) - } - - Button( - onClick = onUninstall, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.kpm_uninstall)) - } - } - } - } -} - -private fun checkStringsCommand(tempFile: File): Int { - val shell = getRootShell() - val command = "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='" - val result = shell.newJob().add(command).to(ArrayList(), null).exec() - - if (!result.isSuccess) { - return 0 - } - - var matchCount = 0 - val keywords = listOf("name=", "version=", "license=", "author=") - var nameExists = false - - for (line in result.out) { - if (!nameExists && line.startsWith("name=")) { - nameExists = true - matchCount++ - } else if (nameExists) { - for (keyword in keywords) { - if (line.startsWith(keyword)) { - matchCount++ - break - } - } - } - } - - return if (nameExists) matchCount else 0 -} - -private fun isElfFile(tempFile: File): Boolean { - val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte()) - val fileBytes = ByteArray(4) - FileInputStream(tempFile).use { input -> - input.read(fileBytes) - } - return fileBytes.contentEquals(elfMagic) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt deleted file mode 100644 index 929a8b21..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt +++ /dev/null @@ -1,941 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.Context -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.* -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.time.* -import java.time.format.DateTimeFormatter -import android.os.Process.myUid -import androidx.core.content.edit - -private val SPACING_SMALL = 4.dp -private val SPACING_MEDIUM = 8.dp -private val SPACING_LARGE = 16.dp - -private const val PAGE_SIZE = 10000 -private const val MAX_TOTAL_LOGS = 100000 - -private const val LOGS_PATCH = "/data/adb/ksu/log/sulog.log" - -data class LogEntry( - val timestamp: String, - val type: LogType, - val uid: String, - val comm: String, - val details: String, - val pid: String, - val rawLine: String -) - -data class LogPageInfo( - val currentPage: Int = 0, - val totalPages: Int = 0, - val totalLogs: Int = 0, - val hasMore: Boolean = false -) - -enum class LogType(val displayName: String, val color: Color) { - SU_GRANT("SU_GRANT", Color(0xFF4CAF50)), - SU_EXEC("SU_EXEC", Color(0xFF2196F3)), - PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)), - SYSCALL("SYSCALL", Color(0xFF00BCD4)), - MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)), - UNKNOWN("UNKNOWN", Color(0xFF757575)) -} - -enum class LogExclType(val displayName: String, val color: Color) { - CURRENT_APP("Current app", Color(0xFF9E9E9E)), - PRCTL_STAR("prctl_*", Color(0xFF00BCD4)), - PRCTL_UNKNOWN("prctl_unknown", Color(0xFF00BCD4)), - SETUID("setuid", Color(0xFF00BCD4)) -} - -private val utcFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") -private val localFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - -private fun saveExcludedSubTypes(context: Context, types: Set) { - val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) - val nameSet = types.map { it.name }.toSet() - prefs.edit { putStringSet("excluded_subtypes", nameSet) } -} - -private fun loadExcludedSubTypes(context: Context): Set { - val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) - val nameSet = prefs.getStringSet("excluded_subtypes", emptySet()) ?: emptySet() - return nameSet.mapNotNull { name -> - LogExclType.entries.firstOrNull { it.name == name } - }.toSet() -} - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun LogViewerScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current - val context = LocalContext.current - val scope = rememberCoroutineScope() - - var logEntries by remember { mutableStateOf>(emptyList()) } - var isLoading by remember { mutableStateOf(false) } - var filterType by rememberSaveable { mutableStateOf(null) } - var searchQuery by rememberSaveable { mutableStateOf("") } - var showSearchBar by rememberSaveable { mutableStateOf(false) } - var pageInfo by remember { mutableStateOf(LogPageInfo()) } - var lastLogFileHash by remember { mutableStateOf("") } - val currentUid = remember { myUid().toString() } - - val initialExcluded = remember { - loadExcludedSubTypes(context) - } - - var excludedSubTypes by rememberSaveable { mutableStateOf(initialExcluded) } - - LaunchedEffect(excludedSubTypes) { - saveExcludedSubTypes(context, excludedSubTypes) - } - - val filteredEntries = remember( - logEntries, filterType, searchQuery, excludedSubTypes - ) { - logEntries.filter { entry -> - val matchesSearch = searchQuery.isEmpty() || - entry.comm.contains(searchQuery, ignoreCase = true) || - entry.details.contains(searchQuery, ignoreCase = true) || - entry.uid.contains(searchQuery, ignoreCase = true) - - // 排除本应用 - if (LogExclType.CURRENT_APP in excludedSubTypes && entry.uid == currentUid) return@filter false - - // 排除 SYSCALL 子类型 - if (entry.type == LogType.SYSCALL) { - val detail = entry.details - if (LogExclType.PRCTL_STAR in excludedSubTypes && detail.startsWith("Syscall: prctl") && !detail.startsWith("Syscall: prctl_unknown")) return@filter false - if (LogExclType.PRCTL_UNKNOWN in excludedSubTypes && detail.startsWith("Syscall: prctl_unknown")) return@filter false - if (LogExclType.SETUID in excludedSubTypes && detail.startsWith("Syscall: setuid")) return@filter false - } - - // 普通类型筛选 - val matchesFilter = filterType == null || entry.type == filterType - matchesFilter && matchesSearch - } - } - - val loadingDialog = rememberLoadingDialog() - val confirmDialog = rememberConfirmDialog() - - val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh -> - scope.launch { - if (isLoading) return@launch - - isLoading = true - try { - loadLogsWithPagination( - page, - forceRefresh, - lastLogFileHash - ) { entries, newPageInfo, newHash -> - logEntries = if (page == 0 || forceRefresh) { - entries - } else { - logEntries + entries - } - pageInfo = newPageInfo - lastLogFileHash = newHash - } - } finally { - isLoading = false - } - } - } - - val onManualRefresh: () -> Unit = { - loadPage(0, true) - } - - val loadNextPage: () -> Unit = { - if (pageInfo.hasMore && !isLoading) { - loadPage(pageInfo.currentPage + 1, false) - } - } - - LaunchedEffect(Unit) { - while (true) { - delay(5_000) - if (!isLoading) { - scope.launch { - val hasNewLogs = checkForNewLogs(lastLogFileHash) - if (hasNewLogs) { - loadPage(0, true) - } - } - } - } - } - - LaunchedEffect(Unit) { - loadPage(0, true) - } - - Scaffold( - topBar = { - LogViewerTopBar( - scrollBehavior = scrollBehavior, - onBackClick = { navigator.navigateUp() }, - showSearchBar = showSearchBar, - searchQuery = searchQuery, - onSearchQueryChange = { searchQuery = it }, - onSearchToggle = { showSearchBar = !showSearchBar }, - onRefresh = onManualRefresh, - onClearLogs = { - scope.launch { - val result = confirmDialog.awaitConfirm( - title = context.getString(R.string.log_viewer_clear_logs), - content = context.getString(R.string.log_viewer_clear_logs_confirm) - ) - if (result == ConfirmResult.Confirmed) { - loadingDialog.withLoading { - clearLogs() - loadPage(0, true) - } - snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared)) - } - } - } - ) - }, - snackbarHost = { SnackbarHost(snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - .nestedScroll(scrollBehavior.nestedScrollConnection) - ) { - LogControlPanel( - filterType = filterType, - onFilterTypeSelected = { filterType = it }, - logCount = filteredEntries.size, - totalCount = logEntries.size, - pageInfo = pageInfo, - excludedSubTypes = excludedSubTypes, - onExcludeToggle = { excl -> - excludedSubTypes = if (excl in excludedSubTypes) - excludedSubTypes - excl - else - excludedSubTypes + excl - } - ) - - // 日志列表 - if (isLoading && logEntries.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else if (filteredEntries.isEmpty()) { - EmptyLogState( - hasLogs = logEntries.isNotEmpty(), - onRefresh = onManualRefresh - ) - } else { - LogList( - entries = filteredEntries, - pageInfo = pageInfo, - isLoading = isLoading, - onLoadMore = loadNextPage, - modifier = Modifier.fillMaxSize() - ) - } - } - } -} - -@Composable -private fun LogControlPanel( - filterType: LogType?, - onFilterTypeSelected: (LogType?) -> Unit, - logCount: Int, - totalCount: Int, - pageInfo: LogPageInfo, - excludedSubTypes: Set, - onExcludeToggle: (LogExclType) -> Unit -) { - var isExpanded by rememberSaveable { mutableStateOf(true) } - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), - elevation = getCardElevation() - ) { - Column { - // 标题栏(点击展开/收起) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isExpanded = !isExpanded } - .padding(SPACING_LARGE), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.settings), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - Icon( - imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - - AnimatedVisibility( - visible = isExpanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier.padding(horizontal = SPACING_LARGE) - ) { - // 类型过滤 - Text( - text = stringResource(R.string.log_viewer_filter_type), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { - item { - FilterChip( - onClick = { onFilterTypeSelected(null) }, - label = { Text(stringResource(R.string.log_viewer_all_types)) }, - selected = filterType == null - ) - } - items(LogType.entries.toTypedArray()) { type -> - FilterChip( - onClick = { onFilterTypeSelected(if (filterType == type) null else type) }, - label = { Text(type.displayName) }, - selected = filterType == type, - leadingIcon = { - Box( - modifier = Modifier - .size(8.dp) - .background(type.color, RoundedCornerShape(4.dp)) - ) - } - ) - } - } - - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - - // 排除子类型 - Text( - text = stringResource(R.string.log_viewer_exclude_subtypes), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { - items(LogExclType.entries.toTypedArray()) { excl -> - val label = if (excl == LogExclType.CURRENT_APP) - stringResource(R.string.log_viewer_exclude_current_app) - else excl.displayName - - FilterChip( - onClick = { onExcludeToggle(excl) }, - label = { Text(label) }, - selected = excl in excludedSubTypes, - leadingIcon = { - Box( - modifier = Modifier - .size(8.dp) - .background(excl.color, RoundedCornerShape(4.dp)) - ) - } - ) - } - } - - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - - // 统计信息 - Column(verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)) { - Text( - text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - if (pageInfo.totalPages > 0) { - Text( - text = stringResource( - R.string.log_viewer_page_info, - pageInfo.currentPage + 1, - pageInfo.totalPages, - pageInfo.totalLogs - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) { - Text( - text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - } - - Spacer(modifier = Modifier.height(SPACING_LARGE)) - } - } - } - } -} - -@Composable -private fun LogList( - entries: List, - pageInfo: LogPageInfo, - isLoading: Boolean, - onLoadMore: () -> Unit, - modifier: Modifier = Modifier -) { - val listState = rememberLazyListState() - - LazyColumn( - state = listState, - modifier = modifier, - contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - verticalArrangement = Arrangement.spacedBy(SPACING_SMALL) - ) { - items(entries) { entry -> - LogEntryCard(entry = entry) - } - - // 加载更多按钮或加载指示器 - if (pageInfo.hasMore) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(SPACING_LARGE), - contentAlignment = Alignment.Center - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp) - ) - } else { - Button( - onClick = onLoadMore, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = Icons.Filled.ExpandMore, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(SPACING_MEDIUM)) - Text(stringResource(R.string.log_viewer_load_more)) - } - } - } - } - } else if (entries.isNotEmpty()) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(SPACING_LARGE), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.log_viewer_all_logs_loaded), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } -} - -@Composable -private fun LogEntryCard(entry: LogEntry) { - var expanded by remember { mutableStateOf(false) } - - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded }, - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) - ) { - Column( - modifier = Modifier.padding(SPACING_MEDIUM) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) - ) { - Box( - modifier = Modifier - .size(12.dp) - .background(entry.type.color, RoundedCornerShape(6.dp)) - ) - Text( - text = entry.type.displayName, - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Bold - ) - } - Text( - text = entry.timestamp, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(SPACING_SMALL)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "UID: ${entry.uid}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "PID: ${entry.pid}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Text( - text = entry.comm, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - maxLines = if (expanded) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis - ) - - if (entry.details.isNotEmpty()) { - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = entry.details, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = if (expanded) Int.MAX_VALUE else 2, - overflow = TextOverflow.Ellipsis - ) - } - - AnimatedVisibility( - visible = expanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column { - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - Text( - text = stringResource(R.string.log_viewer_raw_log), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = entry.rawLine, - style = MaterialTheme.typography.bodySmall, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } -} - -@Composable -private fun EmptyLogState( - hasLogs: Boolean, - onRefresh: () -> Unit -) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(SPACING_LARGE) - ) { - Icon( - imageVector = if (hasLogs) Icons.Filled.FilterList else Icons.Filled.Description, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource( - if (hasLogs) R.string.log_viewer_no_matching_logs - else R.string.log_viewer_no_logs - ), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Button(onClick = onRefresh) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(SPACING_MEDIUM)) - Text(stringResource(R.string.log_viewer_refresh)) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun LogViewerTopBar( - scrollBehavior: TopAppBarScrollBehavior? = null, - onBackClick: () -> Unit, - showSearchBar: Boolean, - searchQuery: String, - onSearchQueryChange: (String) -> Unit, - onSearchToggle: () -> Unit, - onRefresh: () -> Unit, - onClearLogs: () -> Unit -) { - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - - Column { - TopAppBar( - title = { - Text( - text = stringResource(R.string.log_viewer_title), - style = MaterialTheme.typography.titleLarge - ) - }, - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.log_viewer_back) - ) - } - }, - actions = { - IconButton(onClick = onSearchToggle) { - Icon( - imageVector = if (showSearchBar) Icons.Filled.SearchOff else Icons.Filled.Search, - contentDescription = stringResource(R.string.log_viewer_search) - ) - } - IconButton(onClick = onRefresh) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(R.string.log_viewer_refresh) - ) - } - IconButton(onClick = onClearLogs) { - Icon( - imageVector = Icons.Filled.DeleteSweep, - contentDescription = stringResource(R.string.log_viewer_clear_logs) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) - - AnimatedVisibility( - visible = showSearchBar, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - OutlinedTextField( - value = searchQuery, - onValueChange = onSearchQueryChange, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - placeholder = { Text(stringResource(R.string.log_viewer_search_placeholder)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null - ) - }, - trailingIcon = { - if (searchQuery.isNotEmpty()) { - IconButton(onClick = { onSearchQueryChange("") }) { - Icon( - imageVector = Icons.Filled.Clear, - contentDescription = stringResource(R.string.log_viewer_clear_search) - ) - } - } - }, - singleLine = true - ) - } - } -} - -private suspend fun checkForNewLogs( - lastHash: String -): Boolean { - return withContext(Dispatchers.IO) { - try { - val shell = getRootShell() - val logPath = "/data/adb/ksu/log/sulog.log" - - val result = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'") - val currentHash = result.trim() - - currentHash != lastHash && currentHash != "0 0" - } catch (_: Exception) { - false - } - } -} - -private suspend fun loadLogsWithPagination( - page: Int, - forceRefresh: Boolean, - lastHash: String, - onLoaded: (List, LogPageInfo, String) -> Unit -) { - withContext(Dispatchers.IO) { - try { - val shell = getRootShell() - - // 获取文件信息 - val statResult = runCmd(shell, "stat -c '%Y %s' $LOGS_PATCH 2>/dev/null || echo '0 0'") - val currentHash = statResult.trim() - - if (!forceRefresh && currentHash == lastHash && currentHash != "0 0") { - withContext(Dispatchers.Main) { - onLoaded(emptyList(), LogPageInfo(), currentHash) - } - return@withContext - } - - // 获取总行数 - val totalLinesResult = runCmd(shell, "wc -l < $LOGS_PATCH 2>/dev/null || echo '0'") - val totalLines = totalLinesResult.trim().toIntOrNull() ?: 0 - - if (totalLines == 0) { - withContext(Dispatchers.Main) { - onLoaded(emptyList(), LogPageInfo(), currentHash) - } - return@withContext - } - - // 限制最大日志数量 - val effectiveTotal = minOf(totalLines, MAX_TOTAL_LOGS) - val totalPages = (effectiveTotal + PAGE_SIZE - 1) / PAGE_SIZE - - // 计算要读取的行数范围 - val startLine = if (page == 0) { - maxOf(1, totalLines - effectiveTotal + 1) - } else { - val skipLines = page * PAGE_SIZE - maxOf(1, totalLines - effectiveTotal + 1 + skipLines) - } - - val endLine = minOf(startLine + PAGE_SIZE - 1, totalLines) - - if (startLine > totalLines) { - withContext(Dispatchers.Main) { - onLoaded(emptyList(), LogPageInfo(page, totalPages, effectiveTotal, false), currentHash) - } - return@withContext - } - - val result = runCmd(shell, "sed -n '${startLine},${endLine}p' $LOGS_PATCH 2>/dev/null || echo ''") - val entries = parseLogEntries(result) - - val hasMore = endLine < totalLines - val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore) - - withContext(Dispatchers.Main) { - onLoaded(entries, pageInfo, currentHash) - } - } catch (_: Exception) { - withContext(Dispatchers.Main) { - onLoaded(emptyList(), LogPageInfo(), lastHash) - } - } - } -} - -private suspend fun clearLogs() { - withContext(Dispatchers.IO) { - try { - val shell = getRootShell() - runCmd(shell, "echo '' > $LOGS_PATCH") - } catch (_: Exception) { - } - } -} - -private fun parseLogEntries(logContent: String): List { - if (logContent.isBlank()) return emptyList() - - val entries = logContent.lines() - .filter { it.isNotBlank() && it.startsWith("[") } - .mapNotNull { line -> - try { - parseLogLine(line) - } catch (_: Exception) { - null - } - } - - return entries.reversed() -} -private fun utcToLocal(utc: String): String { - return try { - val instant = LocalDateTime.parse(utc, utcFormatter).atOffset(ZoneOffset.UTC).toInstant() - val local = instant.atZone(ZoneId.systemDefault()) - local.format(localFormatter) - } catch (_: Exception) { - utc - } -} - -private fun parseLogLine(line: String): LogEntry? { - // 解析格式: [timestamp] TYPE: UID=xxx COMM=xxx ... - val timestampRegex = """\[(.*?)]""".toRegex() - val timestampMatch = timestampRegex.find(line) ?: return null - val timestamp = utcToLocal(timestampMatch.groupValues[1]) - - val afterTimestamp = line.substring(timestampMatch.range.last + 1).trim() - val parts = afterTimestamp.split(":") - if (parts.size < 2) return null - - val typeStr = parts[0].trim() - val type = when (typeStr) { - "SU_GRANT" -> LogType.SU_GRANT - "SU_EXEC" -> LogType.SU_EXEC - "PERM_CHECK" -> LogType.PERM_CHECK - "SYSCALL" -> LogType.SYSCALL - "MANAGER_OP" -> LogType.MANAGER_OP - else -> LogType.UNKNOWN - } - - val details = parts[1].trim() - val uid: String = extractValue(details, "UID") ?: "" - val comm: String = extractValue(details, "COMM") ?: "" - val pid: String = extractValue(details, "PID") ?: "" - - // 构建详细信息字符串 - val detailsStr = when (type) { - LogType.SU_GRANT -> { - val method: String = extractValue(details, "METHOD") ?: "" - "Method: $method" - } - LogType.SU_EXEC -> { - val target: String = extractValue(details, "TARGET") ?: "" - val result: String = extractValue(details, "RESULT") ?: "" - "Target: $target, Result: $result" - } - LogType.PERM_CHECK -> { - val result: String = extractValue(details, "RESULT") ?: "" - "Result: $result" - } - LogType.SYSCALL -> { - val syscall = extractValue(details, "SYSCALL") ?: "" - val args = extractValue(details, "ARGS") ?: "" - "Syscall: $syscall, Args: $args" - } - LogType.MANAGER_OP -> { - val op: String = extractValue(details, "OP") ?: "" - val managerUid: String = extractValue(details, "MANAGER_UID") ?: "" - val targetUid: String = extractValue(details, "TARGET_UID") ?: "" - "Operation: $op, Manager UID: $managerUid, Target UID: $targetUid" - } - else -> details - } - - return LogEntry( - timestamp = timestamp, - type = type, - uid = uid, - comm = comm, - details = detailsStr, - pid = pid, - rawLine = line - ) -} - -private fun extractValue(text: String, key: String): String? { - val regex = """$key=(\S+)""".toRegex() - return regex.find(text)?.groupValues?.get(1) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 0c203cbd..508b1fa2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -1,787 +1,235 @@ package com.sukisu.ultra.ui.screen -import android.annotation.SuppressLint -import android.app.Activity.* -import android.content.ClipData -import android.content.ClipboardManager +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.isSystemInDarkTheme +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.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Wysiwyg -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Verified -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* +import androidx.compose.material.icons.automirrored.outlined.Undo +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +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.rotate -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel -import com.dergoogler.mmrl.platform.Platform -import com.dergoogler.mmrl.platform.model.ModuleConfig -import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph +import com.kyant.capsule.ContinuousRoundedRectangle import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.* -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.module.ModuleModify -import com.sukisu.ultra.ui.util.module.ModuleOperationUtils -import com.sukisu.ultra.ui.util.module.ModuleUtils -import com.sukisu.ultra.ui.util.module.verifyModuleSignature -import com.sukisu.ultra.ui.viewmodel.ModuleViewModel -import com.sukisu.ultra.ui.webui.WebUIActivity -import com.sukisu.ultra.ui.webui.WebUIXActivity +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import java.util.concurrent.TimeUnit +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ksuApp +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.component.DropdownImpl +import com.sukisu.ultra.ui.component.RebootListPopup +import com.sukisu.ultra.ui.component.SearchBox +import com.sukisu.ultra.ui.component.SearchPager +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.component.rememberLoadingDialog +import com.sukisu.ultra.ui.util.DownloadListener +import com.sukisu.ultra.ui.util.download +import com.sukisu.ultra.ui.util.getFileName +import com.sukisu.ultra.ui.util.hasMagisk +import com.sukisu.ultra.ui.util.toggleModule +import com.sukisu.ultra.ui.util.undoUninstallModule +import com.sukisu.ultra.ui.util.uninstallModule +import com.sukisu.ultra.ui.viewmodel.ModuleViewModel +import com.sukisu.ultra.ui.webui.WebUIActivity +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.HorizontalDivider +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.Switch +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -data class ModuleBottomSheetMenuItem( - val icon: ImageVector, - val titleRes: Int, - val onClick: () -> Unit -) - -/** - * @author ShirkNeko - * @date 2025/9/29. - */ -@SuppressLint("ResourceType", "AutoboxingStateCreation") -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable -fun ModuleScreen(navigator: DestinationsNavigator) { +fun ModulePager( + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { val viewModel = viewModel() - val context = LocalContext.current - val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) - val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() - val confirmDialog = rememberConfirmDialog() - var lastClickTime by remember { mutableStateOf(0L) } + val searchStatus by viewModel.searchStatus - var showSignatureDialog by remember { mutableStateOf(false) } - var signatureDialogMessage by remember { mutableStateOf("") } - var isForceVerificationFailed by remember { mutableStateOf(false) } - var pendingInstallAction by remember { mutableStateOf<(() -> Unit)?>(null) } + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - LaunchedEffect(Unit) { - viewModel.initializeCache(context) - } + val modules = viewModel.moduleList - val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - var showBottomSheet by remember { mutableStateOf(false) } - val listState = rememberLazyListState() - val fabVisible by rememberFabVisibilityState(listState) - - val selectZipLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != RESULT_OK) { - return@rememberLauncherForActivityResult - } - val data = it.data ?: return@rememberLauncherForActivityResult - - scope.launch { - val clipData = data.clipData - if (clipData != null) { - val selectedModules = mutableListOf() - val selectedModuleNames = mutableMapOf() - - fun processUri(uri: Uri) { - try { - if (!ModuleUtils.isUriAccessible(context, uri)) { - return - } - ModuleUtils.takePersistableUriPermission(context, uri) - val moduleName = ModuleUtils.extractModuleName(context, uri) - selectedModules.add(uri) - selectedModuleNames[uri] = moduleName - } catch (e: Exception) { - Log.e("ModuleScreen", "Error while processing URI: $uri, Error: ${e.message}") - } - } - - for (i in 0 until clipData.itemCount) { - val uri = clipData.getItemAt(i).uri - processUri(uri) - } - - if (selectedModules.isEmpty()) { - snackBarHost.showSnackbar("Unable to access selected module files") - return@launch - } - - val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ") - val confirmResult = confirmDialog.awaitConfirm( - title = context.getString(R.string.module_install), - content = context.getString(R.string.module_install_multiple_confirm_with_names, selectedModules.size, modulesList), - confirm = context.getString(R.string.install), - dismiss = context.getString(R.string.cancel) - ) - - if (confirmResult == ConfirmResult.Confirmed) { - // 验证模块签名 - val forceVerification = prefs.getBoolean("force_signature_verification", false) - val verificationResults = mutableMapOf() - - for (uri in selectedModules) { - val isVerified = verifyModuleSignature(context, uri) - verificationResults[uri] = isVerified - // 存储验证状态 - setModuleVerificationStatus(uri, isVerified) - - if (forceVerification && !isVerified) { - withContext(Dispatchers.Main) { - signatureDialogMessage = context.getString(R.string.module_signature_invalid_message) - isForceVerificationFailed = true - showSignatureDialog = true - } - return@launch - } else if (!isVerified) { - withContext(Dispatchers.Main) { - signatureDialogMessage = context.getString(R.string.module_signature_verification_failed) - isForceVerificationFailed = false - pendingInstallAction = { - try { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules))) - viewModel.markNeedRefresh() - } catch (e: Exception) { - Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}") - scope.launch { - snackBarHost.showSnackbar("Error while installing module: ${e.message}") - } - } - } - showSignatureDialog = true - } - return@launch - } - } - - // 所有模块签名验证通过,直接安装 - if (verificationResults.all { it -> it.value }) { - try { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules))) - viewModel.markNeedRefresh() - } catch (e: Exception) { - Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}") - snackBarHost.showSnackbar("Error while installing module: ${e.message}") - } - } - } - } else { - val uri = data.data ?: return@launch - // 单个安装模块 - try { - if (!ModuleUtils.isUriAccessible(context, uri)) { - snackBarHost.showSnackbar("Unable to access selected module files") - return@launch - } - - ModuleUtils.takePersistableUriPermission(context, uri) - - val moduleName = ModuleUtils.extractModuleName(context, uri) - - val confirmResult = confirmDialog.awaitConfirm( - title = context.getString(R.string.module_install), - content = context.getString(R.string.module_install_confirm, moduleName), - confirm = context.getString(R.string.install), - dismiss = context.getString(R.string.cancel) - ) - - if (confirmResult == ConfirmResult.Confirmed) { - // 验证模块签名 - val forceVerification = prefs.getBoolean("force_signature_verification", false) - val isVerified = verifyModuleSignature(context, uri) - // 存储验证状态 - setModuleVerificationStatus(uri, isVerified) - - if (forceVerification && !isVerified) { - signatureDialogMessage = context.getString(R.string.module_signature_invalid_message) - isForceVerificationFailed = true - showSignatureDialog = true - return@launch - } else if (!isVerified) { - signatureDialogMessage = context.getString(R.string.module_signature_verification_failed) - isForceVerificationFailed = false - pendingInstallAction = { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - viewModel.markNeedRefresh() - } - showSignatureDialog = true - return@launch - } - - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - viewModel.markNeedRefresh() - } - } catch (e: Exception) { - Log.e("ModuleScreen", "Error processing a single URI: $uri, Error: ${e.message}") - snackBarHost.showSnackbar("Error processing module file: ${e.message}") - } - } - } - } - - val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) - val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) - - LaunchedEffect(Unit) { - if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { + LaunchedEffect(navigator) { + if (viewModel.moduleList.isEmpty() || viewModel.searchResults.value.isEmpty() || viewModel.isNeedRefresh) { + viewModel.checkModuleUpdate = prefs.getBoolean("module_check_update", true) viewModel.sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false) viewModel.sortActionFirst = prefs.getBoolean("module_sort_action_first", false) viewModel.fetchModuleList() } } - val isSafeMode = Natives.isSafeMode - val hasMagisk = hasMagisk() - val hideInstallButton = isSafeMode || hasMagisk + LaunchedEffect(searchStatus.searchText) { + viewModel.updateSearchText(searchStatus.searchText) + } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + LaunchedEffect(modules) { + viewModel.syncModuleUpdateInfo(modules) + if (searchStatus.searchText.isNotEmpty()) { + viewModel.updateSearchText(searchStatus.searchText) + } + } val webUILauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } - val bottomSheetMenuItems = remember { - listOf( - ModuleBottomSheetMenuItem( - icon = Icons.Outlined.Save, - titleRes = R.string.backup_modules, - onClick = { - backupLauncher.launch(ModuleModify.createBackupIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - ModuleBottomSheetMenuItem( - icon = Icons.Outlined.RestoreFromTrash, - titleRes = R.string.restore_modules, - onClick = { - restoreLauncher.launch(ModuleModify.createRestoreIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ) - ) + val loadingDialog = rememberLoadingDialog() + val confirmDialog = rememberConfirmDialog() + + val isSafeMode = Natives.isSafeMode + val magiskInstalled by produceState(initialValue = false) { + value = withContext(Dispatchers.IO) { hasMagisk() } + } + val hideInstallButton = isSafeMode || magiskInstalled + + val scrollBehavior = MiuixScrollBehavior() + val listState = rememberLazyListState() + var fabVisible by remember { mutableStateOf(true) } + var scrollDistance by remember { mutableFloatStateOf(0f) } + val dynamicTopPadding by remember { + derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) } } - Scaffold( - topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.module)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - dropdownContent = { - IconButton( - onClick = { showBottomSheet = true }, - ) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(id = R.string.settings), - ) - } - }, - scrollBehavior = scrollBehavior, - ) - }, - floatingActionButton = { - AnimatedFab(visible = !hideInstallButton && fabVisible) { - FloatingActionButton( - contentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = MaterialTheme.colorScheme.primary, - onClick = { - selectZipLauncher.launch( - Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/zip" - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - } - ) - }, - content = { - Icon( - painter = painterResource(id = R.drawable.package_import), - contentDescription = null - ) - } - ) - } - }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), - snackbarHost = { SnackbarHost(hostState = snackBarHost) } - ) { innerPadding -> - when { - hasMagisk -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - modifier = Modifier - .size(64.dp) - .padding(bottom = 16.dp) - ) - Text( - stringResource(R.string.module_magisk_conflict), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - } - } - } - else -> { - ModuleList( - navigator = navigator, - viewModel = viewModel, - listState = listState, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - boxModifier = Modifier.padding(innerPadding), - onInstallModule = { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it))) - }, - onUpdateModule = { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModuleUpdate(it))) - }, - onClickModule = { id, name, hasWebUi -> - val currentTime = System.currentTimeMillis() - if (currentTime - lastClickTime < 600) { - Log.d("ModuleScreen", "Click too fast, ignoring") - return@ModuleList - } - lastClickTime = currentTime - - if (hasWebUi) { - try { - val wxEngine = Intent(context, WebUIXActivity::class.java) - .setData("kernelsu://webuix/$id".toUri()) - .putExtra("id", id) - .putExtra("name", name) - - val ksuEngine = Intent(context, WebUIActivity::class.java) - .setData("kernelsu://webui/$id".toUri()) - .putExtra("id", id) - .putExtra("name", name) - - val config = try { - id.asModuleConfig - } catch (e: Exception) { - Log.e("ModuleScreen", "Failed to get config from id: $id", e) - null - } - - val globalEngine = prefs.getString("webui_engine", "default") ?: "default" - val moduleEngine = config?.getWebuiEngine(context) - val selectedEngine = when (globalEngine) { - "wx" -> wxEngine - "ksu" -> ksuEngine - "default" -> { - when (moduleEngine) { - "wx" -> wxEngine - "ksu" -> ksuEngine - else -> { - if (Platform.isAlive) { - wxEngine - } else { - ksuEngine - } - } - } - } - else -> ksuEngine - } - webUILauncher.launch(selectedEngine) - } catch (e: Exception) { - Log.e("ModuleScreen", "Error launching WebUI: ${e.message}", e) - scope.launch { - snackBarHost.showSnackbar("Error launching WebUI: ${e.message}") - } - } - return@ModuleList - } - }, - context = context, - snackBarHost = snackBarHost - ) - } - } - - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showBottomSheet = false - }, - sheetState = bottomSheetState, - dragHandle = { - Surface( - modifier = Modifier.padding(vertical = 11.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp) - ) { - Box( - Modifier.size( - width = 32.dp, - height = 4.dp - ) - ) - } - } - ) { - ModuleBottomSheetContent( - menuItems = bottomSheetMenuItems, - viewModel = viewModel, - prefs = prefs, - scope = scope, - bottomSheetState = bottomSheetState, - onDismiss = { showBottomSheet = false } - ) - } - } - - // 签名验证弹窗 - if (showSignatureDialog) { - AlertDialog( - onDismissRequest = { showSignatureDialog = false }, - icon = { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - }, - title = { - Text( - text = stringResource(R.string.module_signature_invalid), - color = MaterialTheme.colorScheme.error - ) - }, - text = { - Text(text = signatureDialogMessage) - }, - confirmButton = { - if (isForceVerificationFailed) { - // 强制验证失败,只显示确定按钮 - TextButton( - onClick = { showSignatureDialog = false } - ) { - Text(stringResource(R.string.confirm)) - } - } else { - // 非强制验证失败,显示继续安装按钮 - TextButton( - onClick = { - showSignatureDialog = false - pendingInstallAction?.invoke() - pendingInstallAction = null - } - ) { - Text(stringResource(R.string.install)) - } - } - }, - dismissButton = if (!isForceVerificationFailed) { - { - TextButton( - onClick = { - showSignatureDialog = false - pendingInstallAction = null - } - ) { - Text(stringResource(R.string.cancel)) - } - } - } else { - null - } - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ModuleBottomSheetContent( - menuItems: List, - viewModel: ModuleViewModel, - prefs: android.content.SharedPreferences, - scope: kotlinx.coroutines.CoroutineScope, - bottomSheetState: SheetState, - onDismiss: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) - ) { - // 标题 - Text( - text = stringResource(R.string.menu_options), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) - - // 菜单选项网格 - LazyVerticalGrid( - columns = GridCells.Fixed(4), - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(menuItems) { menuItem -> - ModuleBottomSheetMenuItemView( - menuItem = menuItem - ) - } - } - - // 排序选项 - Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) - - Text( - text = stringResource(R.string.sort_options), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) - - Column( - modifier = Modifier.padding(horizontal = 24.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - // 优先显示有操作的模块 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.module_sort_action_first), - style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = viewModel.sortActionFirst, - onCheckedChange = { checked -> - viewModel.sortActionFirst = checked - prefs.edit { - putBoolean("module_sort_action_first", checked) - } - scope.launch { - viewModel.fetchModuleList() - bottomSheetState.hide() - onDismiss() - } - } - ) - } - - // 优先显示已启用的模块 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.module_sort_enabled_first), - style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = viewModel.sortEnabledFirst, - onCheckedChange = { checked -> - viewModel.sortEnabledFirst = checked - prefs.edit { - putBoolean("module_sort_enabled_first", checked) - } - scope.launch { - viewModel.fetchModuleList() - bottomSheetState.hide() - onDismiss() - } - } - ) - } - } - } -} - -@Composable -private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1.0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "menuItemScale" - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null - ) { menuItem.onClick() } - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Surface( - modifier = Modifier.size(48.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) { - Box( - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = menuItem.icon, - contentDescription = stringResource(menuItem.titleRes), - modifier = Modifier.size(24.dp) - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = stringResource(menuItem.titleRes), - style = MaterialTheme.typography.labelSmall, - textAlign = TextAlign.Center, - maxLines = 2 - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ModuleList( - navigator: DestinationsNavigator, - viewModel: ModuleViewModel, - listState: LazyListState, - modifier: Modifier = Modifier, - boxModifier: Modifier = Modifier, - onInstallModule: (Uri) -> Unit, - onUpdateModule: (Uri) -> Unit, - onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, - context: Context, - snackBarHost: SnackbarHostState -) { val failedEnable = stringResource(R.string.module_failed_to_enable) val failedDisable = stringResource(R.string.module_failed_to_disable) + val failedUndoUninstall = stringResource(R.string.module_undo_uninstall_failed) + val successUndoUninstall = stringResource(R.string.module_undo_uninstall_success) val failedUninstall = stringResource(R.string.module_uninstall_failed) val successUninstall = stringResource(R.string.module_uninstall_success) - val reboot = stringResource(R.string.reboot) val rebootToApply = stringResource(R.string.reboot_to_apply) val moduleStr = stringResource(R.string.module) val uninstall = stringResource(R.string.uninstall) val cancel = stringResource(android.R.string.cancel) val moduleUninstallConfirm = stringResource(R.string.module_uninstall_confirm) + val metaModuleUninstallConfirm = stringResource(R.string.metamodule_uninstall_confirm) val updateText = stringResource(R.string.module_update) val changelogText = stringResource(R.string.module_changelog) val downloadingText = stringResource(R.string.module_downloading) val startDownloadingText = stringResource(R.string.module_start_downloading) val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed) - val downloadErrorText = stringResource(R.string.module_download_error) - - val loadingDialog = rememberLoadingDialog() - val confirmDialog = rememberConfirmDialog() suspend fun onModuleUpdate( module: ModuleViewModel.ModuleInfo, changelogUrl: String, downloadUrl: String, - fileName: String + fileName: String, + context: Context, + onInstallModule: (Uri) -> Unit ) { - val client = OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - - val request = okhttp3.Request.Builder() - .url(changelogUrl) - .header("User-Agent", "SukiSU-Ultra/2.0") - .build() - val changelogResult = loadingDialog.withLoading { withContext(Dispatchers.IO) { runCatching { - client.newCall(request).execute().body!!.string() + ksuApp.okhttpClient.newCall( + okhttp3.Request.Builder().url(changelogUrl).build() + ).execute().body!!.string() } } } @@ -824,489 +272,878 @@ private fun ModuleList( downloadUrl, fileName, downloading, - onDownloaded = { uri -> - // 验证更新模块的签名 - val isVerified = verifyModuleSignature(context, uri) - setModuleVerificationStatus(uri, isVerified) - onUpdateModule(uri) - }, + onDownloaded = onInstallModule, onDownloading = { - launch(Dispatchers.Main) { + scope.launch(Dispatchers.Main) { Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show() } - }, - onError = { errorMsg -> - launch(Dispatchers.Main) { - Toast.makeText(context, "$downloadErrorText: $errorMsg", Toast.LENGTH_LONG).show() - } } ) } } - suspend fun onModuleUninstallClicked(module: ModuleViewModel.ModuleInfo) { - val isUninstall = !module.remove - if (isUninstall) { - val confirmResult = confirmDialog.awaitConfirm( - moduleStr, - content = moduleUninstallConfirm.format(module.name), - confirm = uninstall, - dismiss = cancel - ) - if (confirmResult != ConfirmResult.Confirmed) { - return - } - } + suspend fun onModuleUndoUninstall(module: ModuleViewModel.ModuleInfo) { val success = loadingDialog.withLoading { withContext(Dispatchers.IO) { - if (isUninstall) { - // 卸载时移除验证标志 - ModuleOperationUtils.handleModuleUninstall(module.dirId) - uninstallModule(module.dirId) - } else { - restoreModule(module.dirId) - } + undoUninstallModule(module.id) + } + } + + if (success) { + viewModel.fetchModuleList() + } + val message = if (success) { + successUndoUninstall.format(module.name) + } else { + failedUndoUninstall.format(module.name) + } + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { + val formatter = if (module.metamodule) metaModuleUninstallConfirm else moduleUninstallConfirm + val confirmResult = confirmDialog.awaitConfirm( + moduleStr, + content = formatter.format(module.name), + confirm = uninstall, + dismiss = cancel + ) + if (confirmResult != ConfirmResult.Confirmed) { + return + } + + val success = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + uninstallModule(module.id) } } if (success) { viewModel.fetchModuleList() - viewModel.markNeedRefresh() } - if (!isUninstall) return val message = if (success) { successUninstall.format(module.name) } else { failedUninstall.format(module.name) } - val actionLabel = if (success) { - reboot - } else { - null + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + suspend fun onModuleToggle(module: ModuleViewModel.ModuleInfo) { + val success = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + toggleModule(module.id, !module.enabled) + } } - val result = snackBarHost.showSnackbar( - message = message, - actionLabel = actionLabel, - duration = SnackbarDuration.Long - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() + if (success) { + viewModel.fetchModuleList() + Toast.makeText(context, rebootToApply, Toast.LENGTH_SHORT).show() + } else { + val message = if (module.enabled) failedDisable else failedEnable + Toast.makeText(context, message.format(module.name), Toast.LENGTH_SHORT).show() } } - PullToRefreshBox( - modifier = boxModifier, - onRefresh = { - viewModel.fetchModuleList() - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = remember { - PaddingValues( - start = 16.dp, - top = 16.dp, - end = 16.dp, - bottom = 16.dp + 56.dp + 16.dp + 48.dp + 6.dp /* Scaffold Fab Spacing + Fab container height + SnackBar height */ - ) - }, - ) { - when { - viewModel.moduleList.isEmpty() -> { - item { - Box( - modifier = Modifier.fillParentMaxSize(), - contentAlignment = Alignment.Center + fun onModuleClick(id: String, name: String, hasWebUi: Boolean) { + if (hasWebUi) { + webUILauncher.launch( + Intent(context, WebUIActivity::class.java) + .setData("kernelsu://webui/$id".toUri()) + .putExtra("id", id) + .putExtra("name", name) + ) + } + } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val isScrolledToEnd = + (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 + && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size + ?: 0) < listState.layoutInfo.viewportEndOffset) + val delta = available.y + if (!isScrolledToEnd) { + scrollDistance += delta + if (scrollDistance < -50f) { + if (fabVisible) fabVisible = false + scrollDistance = 0f + } else if (scrollDistance > 50f) { + if (!fabVisible) fabVisible = true + scrollDistance = 0f + } + } + return Offset.Zero + } + } + } + val offsetHeight by animateDpAsState( + targetValue = if (fabVisible) 0.dp else 180.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), + animationSpec = tween(durationMillis = 350) + ) + + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) + + Scaffold( + topBar = { + searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) { + TopAppBar( + color = Color.Transparent, + title = stringResource(R.string.module), + actions = { + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } ) { - 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) + ListPopupColumn { + DropdownImpl( + text = stringResource(R.string.module_sort_action_first), + optionSize = 2, + isSelected = viewModel.sortActionFirst, + onSelectedIndexChange = { + viewModel.sortActionFirst = !viewModel.sortActionFirst + prefs.edit { + putBoolean("module_sort_action_first", viewModel.sortActionFirst) + } + scope.launch { + viewModel.fetchModuleList() + } + showTopPopup.value = false + }, + index = 0 ) - Text( - text = stringResource(R.string.module_empty), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, + DropdownImpl( + text = stringResource(R.string.module_sort_enabled_first), + optionSize = 2, + isSelected = viewModel.sortEnabledFirst, + onSelectedIndexChange = { + viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst + prefs.edit { + putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst) + } + scope.launch { + viewModel.fetchModuleList() + } + showTopPopup.value = false + }, + index = 1 ) } } + IconButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, + contentDescription = stringResource(id = R.string.settings) + ) + } + RebootListPopup( + modifier = Modifier.padding(end = 16.dp), + alignment = PopupPositionProvider.Align.TopRight + ) + }, + scrollBehavior = scrollBehavior + ) + } + }, + floatingActionButton = { + if (!hideInstallButton) { + val moduleInstall = stringResource(id = R.string.module_install) + val confirmTitle = stringResource(R.string.module) + var zipUris by remember { mutableStateOf>(emptyList()) } + val confirmDialog = rememberConfirmDialog( + onConfirm = { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(zipUris))) { + launchSingleTop = true + } + viewModel.markNeedRefresh() + } + ) + val uris = mutableListOf() + val moduleNames = uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("") + val confirmContent = stringResource(R.string.module_install_prompt_with_name, moduleNames) + val selectZipLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode != RESULT_OK) { + return@rememberLauncherForActivityResult + } + val data = it.data ?: return@rememberLauncherForActivityResult + val clipData = data.clipData + + if (clipData != null) { + for (i in 0 until clipData.itemCount) { + clipData.getItemAt(i)?.uri?.let { uris.add(it) } + } + } else { + data.data?.let { uris.add(it) } + } + + if (uris.size == 1) { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(uris.first())))) { + launchSingleTop = true + } + } else if (uris.size > 1) { + // multiple files selected + zipUris = uris + confirmDialog.showConfirm( + title = confirmTitle, + content = confirmContent + ) } } + FloatingActionButton( + modifier = Modifier + .offset(y = offsetHeight) + .padding(bottom = bottomInnerPadding + 20.dp, end = 20.dp) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), + shadowElevation = 0.dp, + onClick = { + // Select the zip files to install + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/zip" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + selectZipLauncher.launch(intent) + }, + content = { + Icon( + Icons.Rounded.Add, + moduleInstall, + modifier = Modifier.size(40.dp), + tint = Color.White + ) + }, + ) + } + }, + popupHost = { + searchStatus.SearchPager( + defaultResult = {}, + searchBarTopPadding = dynamicTopPadding, + ) { + item { + Spacer(Modifier.height(6.dp)) + } + items( + viewModel.searchResults.value, + key = { it.id }, + contentType = { "module" } + ) { module -> + AnimatedVisibility( + visible = viewModel.searchResults.value.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + val itemScope = rememberCoroutineScope() + val updateInfoMap = viewModel.updateInfo + val currentModuleState = rememberUpdatedState(module) + val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleViewModel.ModuleUpdateInfo.Empty - else -> { - items(viewModel.moduleList) { module -> - val scope = rememberCoroutineScope() - val updatedModule by produceState(initialValue = Triple("", "", "")) { - scope.launch(Dispatchers.IO) { - value = viewModel.checkUpdate(module) + val onUninstallClick = remember(module.id, itemScope, ::onModuleUninstall) { + { + itemScope.launch { + onModuleUninstall(currentModuleState.value) + } + Unit + } + } + val onUndoUninstallClick = remember(module.id, itemScope, ::onModuleUndoUninstall) { + { + itemScope.launch { + onModuleUndoUninstall(currentModuleState.value) + } + Unit + } + } + val onToggleClick = remember(module.id, itemScope, ::onModuleToggle) { + { _: Boolean -> + itemScope.launch { + onModuleToggle(currentModuleState.value) + } + Unit + } + } + val onUpdateClick = remember(module.id, moduleUpdateInfo, itemScope, ::onModuleUpdate, context, navigator) { + { + itemScope.launch { + onModuleUpdate( + currentModuleState.value, + moduleUpdateInfo.changelog, + moduleUpdateInfo.downloadUrl, + "${currentModuleState.value.name}-${moduleUpdateInfo.version}.zip", + context + ) { uri -> + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(uri)))) { + launchSingleTop = true + } + } + } + Unit + } + } + val onExecuteActionClick = remember(module.id, navigator, viewModel) { + { + navigator.navigate(ExecuteModuleActionScreenDestination(currentModuleState.value.id)) { + launchSingleTop = true + } + viewModel.markNeedRefresh() + } + } + val onOpenWebUiClick = remember(module.id) { + { + onModuleClick( + currentModuleState.value.id, + currentModuleState.value.name, + currentModuleState.value.hasWebUi + ) + } + } + ModuleItem( + module = module, + updateUrl = moduleUpdateInfo.downloadUrl, + onUndoUninstall = onUndoUninstallClick, + onUninstall = onUninstallClick, + onCheckChanged = onToggleClick, + onUpdate = onUpdateClick, + onExecuteAction = onExecuteActionClick, + onOpenWebUi = onOpenWebUiClick + ) + } + } + item { + val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() + Spacer(Modifier.height(maxOf(bottomInnerPadding, imeBottomPadding))) + } + } + }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + when { + magiskInstalled -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.module_magisk_conflict), + textAlign = TextAlign.Center, + ) + } + } + + else -> { + val layoutDirection = LocalLayoutDirection.current + searchStatus.SearchBox( + searchBarTopPadding = dynamicTopPadding, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + hazeState = hazeState, + hazeStyle = hazeStyle + ) { boxHeight -> + ModuleList( + navigator, + viewModel = viewModel, + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(nestedScrollConnection) + .hazeSource(state = hazeState), + scope = scope, + modules = modules, + onInstallModule = { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(it)))) { + launchSingleTop = true + } + }, + onClickModule = { id, name, hasWebUi -> + onModuleClick(id, name, hasWebUi) + }, + onModuleUninstall = { module -> + onModuleUninstall(module) + }, + onModuleUndoUninstall = { module -> + onModuleUndoUninstall(module) + }, + onModuleToggle = { module -> + onModuleToggle(module) + }, + onModuleUpdate = { module, changelogUrl, downloadUrl, fileName -> + onModuleUpdate( + module, + changelogUrl, + downloadUrl, + fileName, + context + ) { uri -> + navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(listOf(uri)))) { + launchSingleTop = true + } + } + }, + context = context, + innerPadding = innerPadding, + bottomInnerPadding = bottomInnerPadding, + boxHeight = boxHeight + ) + } + } + } + } +} + +@Composable +private fun ModuleList( + navigator: DestinationsNavigator, + viewModel: ModuleViewModel, + modifier: Modifier = Modifier, + scope: CoroutineScope, + modules: List, + onInstallModule: (Uri) -> Unit, + onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, + onModuleUninstall: suspend (ModuleViewModel.ModuleInfo) -> Unit, + onModuleUndoUninstall: suspend (ModuleViewModel.ModuleInfo) -> Unit, + onModuleToggle: suspend (ModuleViewModel.ModuleInfo) -> Unit, + onModuleUpdate: suspend (ModuleViewModel.ModuleInfo, String, String, String) -> Unit, + context: Context, + innerPadding: PaddingValues, + bottomInnerPadding: Dp, + boxHeight: MutableState +) { + val layoutDirection = LocalLayoutDirection.current + val updateInfoMap = viewModel.updateInfo + + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + val refreshTexts = remember { + listOf( + context.getString(R.string.refresh_pulling), + context.getString(R.string.refresh_release), + context.getString(R.string.refresh_refresh), + context.getString(R.string.refresh_complete), + ) + } + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) + viewModel.fetchModuleList() + isRefreshing = false + } + } + + when { + !viewModel.isOverlayAvailable -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = bottomInnerPadding + ), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.module_overlay_fs_not_available), + textAlign = TextAlign.Center, + color = Color.Gray, + ) + } + } + + modules.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = bottomInnerPadding + ), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.module_empty), + textAlign = TextAlign.Center, + color = Color.Gray, + ) + } + } + + else -> { + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { if (!isRefreshing) isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + ), + ) { + LazyColumn( + modifier = modifier.height(getWindowSize().height.dp), + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + ), + overscrollEffect = null, + ) { + items( + items = modules, + key = { it.id }, + contentType = { "module" } + ) { module -> + val currentModuleState = rememberUpdatedState(module) + val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleViewModel.ModuleUpdateInfo.Empty + + val onUndoUninstallClick = remember(module.id, scope, onModuleUndoUninstall) { + { + scope.launch { + onModuleUndoUninstall(currentModuleState.value) + } + Unit + } + } + val onUninstallClick = remember(module.id, scope, onModuleUninstall) { + { + scope.launch { + onModuleUninstall(currentModuleState.value) + } + Unit + } + } + val onToggleClick = remember(module.id, scope, onModuleToggle) { + { _: Boolean -> + scope.launch { + onModuleToggle(currentModuleState.value) + } + Unit + } + } + val onUpdateClick = remember(module.id, moduleUpdateInfo, scope, onModuleUpdate) { + { + scope.launch { + onModuleUpdate( + currentModuleState.value, + moduleUpdateInfo.changelog, + moduleUpdateInfo.downloadUrl, + "${currentModuleState.value.name}-${moduleUpdateInfo.version}.zip", + ) + } + Unit + } + } + val onExecuteActionClick = remember(module.id, navigator, viewModel) { + { + navigator.navigate(ExecuteModuleActionScreenDestination(currentModuleState.value.id)) { + launchSingleTop = true + } + viewModel.markNeedRefresh() + } + } + val onOpenWebUiClick = remember(module.id, onClickModule) { + { + onClickModule( + currentModuleState.value.id, + currentModuleState.value.name, + currentModuleState.value.hasWebUi + ) } } ModuleItem( - navigator = navigator, module = module, - updateUrl = updatedModule.first, - onUninstallClicked = { - scope.launch { onModuleUninstallClicked(module) } - }, - onCheckChanged = { - scope.launch { - val success = withContext(Dispatchers.IO) { - toggleModule(module.dirId, !module.enabled) - } - if (success) { - viewModel.fetchModuleList() - - val result = snackBarHost.showSnackbar( - message = rebootToApply, - actionLabel = reboot, - duration = SnackbarDuration.Long - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } - } else { - val message = if (module.enabled) failedDisable else failedEnable - snackBarHost.showSnackbar(message.format(module.name)) - } - } - }, - onUpdate = { - scope.launch { - onModuleUpdate( - module, - updatedModule.third, - updatedModule.first, - "${module.name}-${updatedModule.second}.zip" - ) - } - }, - onClick = { - onClickModule(it.dirId, it.name, it.hasWebUi) - } + updateUrl = moduleUpdateInfo.downloadUrl, + onUninstall = onUninstallClick, + onUndoUninstall = onUndoUninstallClick, + onCheckChanged = onToggleClick, + onUpdate = onUpdateClick, + onExecuteAction = onExecuteActionClick, + onOpenWebUi = onOpenWebUiClick ) - - Spacer(Modifier.height(1.dp)) + } + item { + Spacer(Modifier.height(bottomInnerPadding)) } } } } - - DownloadListener(context, onInstallModule) } + DownloadListener(context, onInstallModule) } @Composable fun ModuleItem( - navigator: DestinationsNavigator, module: ModuleViewModel.ModuleInfo, updateUrl: String, - onUninstallClicked: (ModuleViewModel.ModuleInfo) -> Unit, + onUndoUninstall: () -> Unit, + onUninstall: () -> Unit, onCheckChanged: (Boolean) -> Unit, - onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, - onClick: (ModuleViewModel.ModuleInfo) -> Unit + onUpdate: () -> Unit, + onExecuteAction: () -> Unit, + onOpenWebUi: () -> Unit ) { - val context = LocalContext.current - val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) - val isHideTagRow = prefs.getBoolean("is_hide_tag_row", false) - // 获取显示更多模块信息的设置 - val showMoreModuleInfo = prefs.getBoolean("show_more_module_info", false) + val isDark = isSystemInDarkTheme() + val hasUpdate by remember(updateUrl) { derivedStateOf { updateUrl.isNotEmpty() } } + val textDecoration by remember(module.remove) { + mutableStateOf(if (module.remove) TextDecoration.LineThrough else null) + } + val onSurface = colorScheme.onSurface + val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f) + val actionIconTint = remember(isDark) { onSurface.copy(alpha = if (isDark) 0.7f else 0.9f) } + val updateBg = remember(isDark) { Color(if (isDark) 0xFF25354E else 0xFFEAF2FF) } + val updateTint = remember { Color(0xFF0D84FF) } - // 剪贴板管理器和触觉反馈 - val clipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - val hapticFeedback = LocalHapticFeedback.current - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = getCardElevation(), + Card( + modifier = Modifier + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + insideMargin = PaddingValues(16.dp) ) { - val textDecoration = if (!module.remove) null else TextDecoration.LineThrough - val interactionSource = remember { MutableInteractionSource() } - val indication = LocalIndication.current - val viewModel = viewModel() - - val sizeStr = remember(module.dirId) { - viewModel.getModuleSize(module.dirId) - } - - Column( - modifier = Modifier - .run { - if (module.hasWebUi) { - toggleable( - value = module.enabled, - enabled = !module.remove && module.enabled, - interactionSource = interactionSource, - role = Role.Button, - indication = indication, - onValueChange = { onClick(module) } - ) - } else { - this - } - } - .padding(22.dp, 18.dp, 22.dp, 12.dp) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + Column( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) ) { val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) - Column( - modifier = Modifier.fillMaxWidth(0.8f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + SubcomposeLayout { constraints -> + val spacingPx = 6.dp.roundToPx() + var nameTextLayout: TextLayoutResult? = null + val metaPlaceable = if (module.metamodule) { + subcompose("meta") { + Text( + text = "META", + fontSize = 12.sp, + color = updateTint, + modifier = Modifier + .clip(ContinuousRoundedRectangle(6.dp)) + .background(updateBg) + .padding(horizontal = 6.dp, vertical = 2.dp), + fontWeight = FontWeight(750), + maxLines = 1, + softWrap = false + ) + }.first().measure(Constraints(0, constraints.maxWidth, 0, constraints.maxHeight)) + } else null + + val reserved = (metaPlaceable?.width ?: 0) + if (metaPlaceable != null) spacingPx else 0 + val nameMax = (constraints.maxWidth - reserved).coerceAtLeast(0) + val namePlaceable = subcompose("name") { Text( text = module.name, - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontWeight = FontWeight.SemiBold, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.titleMedium.fontFamily, + fontSize = 17.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurface, textDecoration = textDecoration, - modifier = Modifier.weight(1f, false) + onTextLayout = { nameTextLayout = it } ) + }.first().measure(Constraints(constraints.minWidth, nameMax, constraints.minHeight, constraints.maxHeight)) - // 显示验证标签 - if (module.isVerified) { - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) { - Icon( - imageVector = Icons.Default.Verified, - contentDescription = stringResource(R.string.module_signature_verified), - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(12.dp) - ) - Spacer(modifier = Modifier.width(2.dp)) - Text( - text = stringResource(R.string.module_verified), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Medium - ) - } - } - } - } + val width = (namePlaceable.width + reserved).coerceIn(constraints.minWidth, constraints.maxWidth) + val height = maxOf(namePlaceable.height, metaPlaceable?.height ?: 0) - Text( - text = "$moduleVersion: ${module.version}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration, - ) - - Text( - text = "$moduleAuthor: ${module.author}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration, - ) - - // 显示更多模块信息时添加updateJson - if (showMoreModuleInfo && module.updateJson.isNotEmpty()) { - val updateJsonLabel = stringResource(R.string.module_update_json) - Text( - text = "$updateJsonLabel: ${module.updateJson}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration, - color = MaterialTheme.colorScheme.primary, - maxLines = 5, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = { }, - onLongClick = { - val clipData = ClipData.newPlainText( - "Update JSON URL", - module.updateJson - ) - clipboardManager.setPrimaryClip(clipData) - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - - Toast.makeText( - context, - context.getString(R.string.module_update_json_copied), - Toast.LENGTH_SHORT - ).show() - } - ), - ) + layout(width, height) { + namePlaceable.placeRelative(0, 0) + val endX = nameTextLayout?.let { layoutRes -> + val last = (layoutRes.lineCount - 1).coerceAtLeast(0) + layoutRes.getLineRight(last).toInt() + } ?: namePlaceable.width + metaPlaceable?.placeRelative(endX + spacingPx, (height - (metaPlaceable.height)) / 2) } } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - Switch( - enabled = !module.update, - checked = module.enabled, - onCheckedChange = onCheckChanged, - interactionSource = if (!module.hasWebUi) interactionSource else null, - ) - } + Text( + text = "$moduleVersion: ${module.version}", + fontSize = 12.sp, + modifier = Modifier.padding(top = 2.dp), + fontWeight = FontWeight(550), + color = colorScheme.onSurfaceVariantSummary, + textDecoration = textDecoration + ) + Text( + text = "$moduleAuthor: ${module.author}", + fontSize = 12.sp, + modifier = Modifier.padding(bottom = 1.dp), + fontWeight = FontWeight(550), + color = colorScheme.onSurfaceVariantSummary, + textDecoration = textDecoration + ) } + Switch( + enabled = !module.update, + checked = module.enabled, + onCheckedChange = { + if (it != module.enabled) onCheckChanged(it) + } + ) + } - Spacer(modifier = Modifier.height(12.dp)) - + if (module.description.isNotBlank()) { Text( text = module.description, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontWeight = MaterialTheme.typography.bodySmall.fontWeight, + fontSize = 14.sp, + color = colorScheme.onSurfaceVariantSummary, + modifier = Modifier.padding(top = 2.dp), overflow = TextOverflow.Ellipsis, maxLines = 4, - textDecoration = textDecoration, + textDecoration = textDecoration ) + } - if (!isHideTagRow) { - Spacer(modifier = Modifier.height(12.dp)) - // 文件夹名称和大小标签 - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - ) { - Text( - text = module.dirId, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), - color = MaterialTheme.colorScheme.onPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = 0.5.dp, + color = colorScheme.outline.copy(alpha = 0.5f) + ) + + Row { + AnimatedVisibility( + visible = module.enabled && !module.remove, + enter = fadeIn(), + exit = fadeOut() + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (module.hasActionScript) { + IconButton( + backgroundColor = secondaryContainer, + minHeight = 35.dp, + minWidth = 35.dp, + onClick = onExecuteAction, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Rounded.PlayArrow, + tint = actionIconTint, + contentDescription = stringResource(R.string.action) + ) + } } - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier - ) { - Text( - text = sizeStr, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), - color = MaterialTheme.colorScheme.onSecondaryContainer, - maxLines = 1 - ) + if (module.hasWebUi) { + IconButton( + backgroundColor = secondaryContainer, + minHeight = 35.dp, + minWidth = 35.dp, + onClick = onOpenWebUi, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Rounded.Code, + tint = actionIconTint, + contentDescription = stringResource(R.string.open) + ) + } } } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(Modifier.weight(1f)) - HorizontalDivider(thickness = Dp.Hairline) - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + AnimatedVisibility( + visible = hasUpdate, + enter = fadeIn(), + exit = fadeOut() ) { - if (module.hasActionScript) { - FilledTonalButton( - 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, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.PlayArrow, - contentDescription = null - ) - } - } - - if (module.hasWebUi) { - FilledTonalButton( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - enabled = !module.remove && module.enabled, - onClick = { onClick(module) }, - interactionSource = interactionSource, - contentPadding = ButtonDefaults.TextButtonContentPadding, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, - contentDescription = null - ) - } - } - - Spacer(modifier = Modifier.weight(1f, true)) - - if (updateUrl.isNotEmpty()) { - Button( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - enabled = !module.remove, - onClick = { onUpdate(module) }, - shape = ButtonDefaults.textShape, - contentPadding = ButtonDefaults.TextButtonContentPadding, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Download, - contentDescription = null - ) - } - } - - FilledTonalButton( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - onClick = { onUninstallClicked(module) }, - contentPadding = ButtonDefaults.TextButtonContentPadding, + IconButton( + modifier = Modifier.padding(end = 16.dp), + backgroundColor = updateBg, + enabled = !module.remove, + minHeight = 35.dp, + minWidth = 35.dp, + onClick = onUpdate, ) { - if (!module.remove) { + Row( + modifier = Modifier.padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { Icon( modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Delete, - contentDescription = null, + imageVector = Icons.Rounded.Download, + tint = updateTint, + contentDescription = stringResource(R.string.module_update), ) - } else { - Icon( - modifier = Modifier.size(20.dp).rotate(180f), - imageVector = Icons.Outlined.Refresh, - contentDescription = null + Text( + modifier = Modifier.padding(end = 3.dp), + text = stringResource(R.string.module_update), + color = updateTint, + fontWeight = FontWeight.Medium, + fontSize = 15.sp + ) + } + } + } + IconButton( + minHeight = 35.dp, + minWidth = 35.dp, + onClick = if (module.remove) onUndoUninstall else onUninstall, + backgroundColor = if (module.remove) { + secondaryContainer.copy(alpha = 0.8f) + } else { + secondaryContainer + }, + ) { + val animatedPadding by animateDpAsState( + targetValue = if (!hasUpdate) 10.dp else 0.dp, + animationSpec = tween(durationMillis = 300) + ) + Row( + modifier = Modifier.padding(horizontal = animatedPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = if (module.remove) { + Icons.AutoMirrored.Outlined.Undo + } else { + Icons.Outlined.Delete + }, + tint = actionIconTint, + contentDescription = null + ) + AnimatedVisibility( + visible = !hasUpdate, + enter = expandHorizontally(), + exit = shrinkHorizontally() + ) { + Text( + modifier = Modifier.padding(start = 4.dp, end = 3.dp), + text = stringResource( + if (module.remove) R.string.undo else R.string.uninstall + ), + color = actionIconTint, + fontWeight = FontWeight.Medium, + fontSize = 15.sp ) } } @@ -1314,27 +1151,3 @@ fun ModuleItem( } } } - -@Preview -@Composable -fun ModuleItemPreview() { - val module = ModuleViewModel.ModuleInfo( - id = "id", - name = "name", - version = "version", - versionCode = 1, - author = "author", - description = "I am a test module and i do nothing but show a very long description", - enabled = true, - update = true, - remove = false, - updateJson = "", - hasWebUi = false, - hasActionScript = false, - dirId = "dirId", - config = ModuleConfig(), - isVerified = true, - verificationTimestamp = System.currentTimeMillis() - ) - ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 22cc0ccd..77bbc722 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -1,142 +1,220 @@ package com.sukisu.ultra.ui.screen import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout +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.systemBars import androidx.compose.foundation.lazy.LazyColumn -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.ArrowForward -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.Adb +import androidx.compose.material.icons.rounded.BugReport +import androidx.compose.material.icons.rounded.ContactPage +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.DeleteForever +import androidx.compose.material.icons.rounded.DeveloperMode import androidx.compose.material.icons.rounded.EnhancedEncryption +import androidx.compose.material.icons.rounded.Fence import androidx.compose.material.icons.rounded.FolderDelete import androidx.compose.material.icons.rounded.RemoveCircle import androidx.compose.material.icons.rounded.RemoveModerator -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Update +import androidx.compose.material.icons.rounded.UploadFile +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider import androidx.core.content.edit -import com.maxkeppeker.sheets.core.models.base.IconSource -import com.maxkeppeler.sheets.list.models.ListOption import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination -import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination -import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination -import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination -import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.BuildConfig +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import com.sukisu.ultra.Natives import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.* -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import com.sukisu.ultra.ui.component.KsuIsValid +import com.sukisu.ultra.ui.component.SendLogDialog +import com.sukisu.ultra.ui.component.SuperDropdown +import com.sukisu.ultra.ui.component.UninstallDialog +import com.sukisu.ultra.ui.component.rememberLoadingDialog +import com.sukisu.ultra.ui.util.execKsud +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperSwitch +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** - * @author ShirkNeko - * @date 2025/9/29. + * @author weishu + * @date 2023/1/1. */ -private val SPACING_SMALL = 3.dp -private val SPACING_MEDIUM = 8.dp -private val SPACING_LARGE = 16.dp - -@OptIn(ExperimentalMaterial3Api::class) -@Destination @Composable -fun SettingScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current - val context = LocalContext.current - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - var isSuLogEnabled by remember { mutableStateOf(Natives.isSuLogEnabled()) } - var selectedEngine by rememberSaveable { - mutableStateOf( - prefs.getString("webui_engine", "default") ?: "default" - ) - } +@Destination +fun SettingPager( + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) Scaffold( topBar = { - TopBar(scrollBehavior = scrollBehavior) + TopAppBar( + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + title = stringResource(R.string.settings), + scrollBehavior = scrollBehavior + ) }, - snackbarHost = { SnackbarHost(snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - val aboutDialog = rememberCustomDialog { - AboutDialog(it) - } + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> val loadingDialog = rememberLoadingDialog() - Column( - modifier = Modifier - .padding(paddingValues) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) - ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val exportBugreportLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.CreateDocument("application/gzip") - ) { uri: Uri? -> - if (uri == null) return@rememberLauncherForActivityResult - scope.launch(Dispatchers.IO) { - loadingDialog.show() - context.contentResolver.openOutputStream(uri)?.use { output -> - getBugreportFile(context).inputStream().use { - it.copyTo(output) - } - } - loadingDialog.hide() - snackBarHost.showSnackbar(context.getString(R.string.log_saved)) - } - } + val showUninstallDialog = rememberSaveable { mutableStateOf(false) } + val uninstallDialog = UninstallDialog(showUninstallDialog, navigator) + val showSendLogDialog = rememberSaveable { mutableStateOf(false) } + val sendLogDialog = SendLogDialog(showSendLogDialog, loadingDialog) - // 配置卡片 - KsuIsValid { - SettingsGroupCard( - title = stringResource(R.string.configuration), - content = { - // 配置文件模板入口 - SettingItem( - icon = Icons.Filled.Fence, - title = stringResource(R.string.settings_profile_template), - summary = stringResource(R.string.settings_profile_template_summary), - onClick = { - navigator.navigate(AppProfileTemplateScreenDestination) + LazyColumn( + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .hazeSource(state = hazeState) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null, + ) { + item { + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + var checkUpdate by rememberSaveable { + mutableStateOf(prefs.getBoolean("check_update", true)) + } + + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + SuperSwitch( + title = stringResource(id = R.string.settings_check_update), + summary = stringResource(id = R.string.settings_check_update_summary), + leftAction = { + Icon( + Icons.Rounded.Update, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_check_update), + tint = colorScheme.onBackground + ) + }, + checked = checkUpdate, + onCheckedChange = { + prefs.edit { + putBoolean("check_update", it) + } + checkUpdate = it + } + ) + KsuIsValid { + var checkModuleUpdate by rememberSaveable { + mutableStateOf(prefs.getBoolean("module_check_update", true)) + } + SuperSwitch( + title = stringResource(id = R.string.settings_module_check_update), + summary = stringResource(id = R.string.settings_check_update_summary), + leftAction = { + Icon( + Icons.Rounded.UploadFile, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_check_update), + tint = colorScheme.onBackground + ) + }, + checked = checkModuleUpdate, + onCheckedChange = { + prefs.edit { + putBoolean("module_check_update", it) + } + checkModuleUpdate = it } ) + } + } + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + val profileTemplate = stringResource(id = R.string.settings_profile_template) + SuperArrow( + title = profileTemplate, + summary = stringResource(id = R.string.settings_profile_template_summary), + leftAction = { + Icon( + Icons.Rounded.Fence, + modifier = Modifier.padding(end = 16.dp), + contentDescription = profileTemplate, + tint = colorScheme.onBackground + ) + }, + onClick = { + navigator.navigate(AppProfileTemplateScreenDestination) { + launchSingleTop = true + } + } + ) + } + } + + KsuIsValid { + + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { val modeItems = listOf( stringResource(id = R.string.settings_mode_default), stringResource(id = R.string.settings_mode_temp_enable), @@ -152,10 +230,17 @@ fun SettingScreen(navigator: DestinationsNavigator) { ) } SuperDropdown( - icon = Icons.Rounded.EnhancedEncryption, title = stringResource(id = R.string.settings_enable_enhanced_security), summary = stringResource(id = R.string.settings_enable_enhanced_security_summary), items = modeItems, + leftAction = { + Icon( + Icons.Rounded.EnhancedEncryption, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_enable_enhanced_security), + tint = colorScheme.onBackground + ) + }, selectedIndex = enhancedSecurityMode, onSelectedIndexChange = { index -> when (index) { @@ -195,10 +280,17 @@ fun SettingScreen(navigator: DestinationsNavigator) { ) } SuperDropdown( - icon = Icons.Rounded.RemoveModerator, title = stringResource(id = R.string.settings_disable_su), summary = stringResource(id = R.string.settings_disable_su_summary), items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveModerator, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_su), + tint = colorScheme.onBackground + ) + }, selectedIndex = suCompatMode, onSelectedIndexChange = { index -> when (index) { @@ -238,10 +330,17 @@ fun SettingScreen(navigator: DestinationsNavigator) { ) } SuperDropdown( - icon = Icons.Rounded.RemoveCircle, title = stringResource(id = R.string.settings_disable_kernel_umount), summary = stringResource(id = R.string.settings_disable_kernel_umount_summary), items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_kernel_umount), + tint = colorScheme.onBackground + ) + }, selectedIndex = kernelUmountMode, onSelectedIndexChange = { index -> when (index) { @@ -270,59 +369,25 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } ) + } - var kernelSuLogMode by rememberSaveable { - mutableIntStateOf( - run { - val currentEnabled = Natives.isSuLogEnabled() - val savedPersist = prefs.getInt("kernel_sulog_mode", 0) - if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 - } - ) - } - SuperDropdown( - icon = Icons.Filled.NoAccounts, - title = stringResource(id = R.string.settings_disable_sulog), - summary = stringResource(id = R.string.settings_disable_sulog_summary), - items = modeItems, - selectedIndex = kernelSuLogMode, - onSelectedIndexChange = { index -> - when (index) { - // Default: enable and save to persist - 0 -> if (Natives.setSuLogEnabled(true)) { - execKsud("feature save", true) - prefs.edit { putInt("kernel_sulog_mode", 0) } - kernelSuLogMode = 0 - isSuLogEnabled = true - } - - // Temporarily disable: save enabled state first, then disable - 1 -> if (Natives.setSuLogEnabled(true)) { - execKsud("feature save", true) - if (Natives.setSuLogEnabled(false)) { - prefs.edit { putInt("kernel_sulog_mode", 0) } - kernelSuLogMode = 1 - isSuLogEnabled = false - } - } - - // Permanently disable: disable and save - 2 -> if (Natives.setSuLogEnabled(false)) { - execKsud("feature save", true) - prefs.edit { putInt("kernel_sulog_mode", 2) } - kernelSuLogMode = 2 - isSuLogEnabled = false - } - } - } - ) - - // 卸载模块开关 + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) } - SwitchItem( - icon = Icons.Rounded.FolderDelete, + SuperSwitch( title = stringResource(id = R.string.settings_umount_modules_default), summary = stringResource(id = R.string.settings_umount_modules_default_summary), + leftAction = { + Icon( + Icons.Rounded.FolderDelete, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_umount_modules_default), + tint = colorScheme.onBackground + ) + }, checked = umountChecked, onCheckedChange = { if (Natives.setDefaultUmountModules(it)) { @@ -330,658 +395,118 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } ) - } - ) - } - // 应用设置卡片 - SettingsGroupCard( - title = stringResource(R.string.app_settings), - content = { - // 更新检查开关 - var checkUpdate by rememberSaveable { - mutableStateOf(prefs.getBoolean("check_update", true)) - } - SwitchItem( - icon = Icons.Filled.Update, - title = stringResource(R.string.settings_check_update), - summary = stringResource(R.string.settings_check_update_summary), - checked = checkUpdate, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("check_update", enabled) } - checkUpdate = enabled + var enableWebDebugging by rememberSaveable { + mutableStateOf(prefs.getBoolean("enable_web_debugging", false)) } - ) - - // WebUI引擎选择 - KsuIsValid { - WebUIEngineSelector( - selectedEngine = selectedEngine, - onEngineSelected = { engine -> - selectedEngine = engine - prefs.edit { putString("webui_engine", engine) } - } - ) - } - - // Web调试和Web X Eruda 开关 - var enableWebDebugging by rememberSaveable { - mutableStateOf(prefs.getBoolean("enable_web_debugging", false)) - } - var useWebUIXEruda by rememberSaveable { - mutableStateOf(prefs.getBoolean("use_webuix_eruda", false)) - } - - KsuIsValid { - SwitchItem( - icon = Icons.Filled.DeveloperMode, - title = stringResource(R.string.enable_web_debugging), - summary = stringResource(R.string.enable_web_debugging_summary), - checked = enableWebDebugging, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("enable_web_debugging", enabled) } - enableWebDebugging = enabled - } - ) - - AnimatedVisibility( - visible = enableWebDebugging && selectedEngine == "wx", - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SwitchItem( - icon = Icons.Filled.FormatListNumbered, - title = stringResource(R.string.use_webuix_eruda), - summary = stringResource(R.string.use_webuix_eruda_summary), - checked = useWebUIXEruda, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("use_webuix_eruda", enabled) } - useWebUIXEruda = enabled - } - ) - } - } - - // 更多设置 - SettingItem( - icon = Icons.Filled.Settings, - title = stringResource(R.string.more_settings), - summary = stringResource(R.string.more_settings), - onClick = { - navigator.navigate(MoreSettingsScreenDestination) - } - ) - } - ) - - // 工具卡片 - SettingsGroupCard( - title = stringResource(R.string.tools), - content = { - var showBottomsheet by remember { mutableStateOf(false) } - - SettingItem( - icon = Icons.Filled.BugReport, - title = stringResource(R.string.send_log), - onClick = { - showBottomsheet = true - } - ) - - // 查看使用日志 - KsuIsValid { - if (isSuLogEnabled) { - SettingItem( - icon = Icons.Filled.Visibility, - title = stringResource(R.string.log_viewer_view_logs), - summary = stringResource(R.string.log_viewer_view_logs_summary), - onClick = { - navigator.navigate(LogViewerScreenDestination) - } - ) - } - } - val lkmMode = Natives.isLkmMode - KsuIsValid { - if (lkmMode) { - SettingItem( - icon = Icons.Filled.FolderOff, - title = stringResource(R.string.umount_path_manager), - summary = stringResource(R.string.umount_path_manager_summary), - onClick = { - navigator.navigate(UmountManagerScreenDestination) - } - ) - } - } - - if (showBottomsheet) { - LogBottomSheet( - onDismiss = { showBottomsheet = false }, - onSaveLog = { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") - val current = LocalDateTime.now().format(formatter) - exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") - showBottomsheet = false + SuperSwitch( + title = stringResource(id = R.string.enable_web_debugging), + summary = stringResource(id = R.string.enable_web_debugging_summary), + leftAction = { + Icon( + Icons.Rounded.DeveloperMode, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.enable_web_debugging), + tint = colorScheme.onBackground + ) }, - onShareLog = { - scope.launch { - val bugreport = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - getBugreportFile(context) - } - } - - val 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 - } + checked = enableWebDebugging, + onCheckedChange = { + prefs.edit { putBoolean("enable_web_debugging", it) } + enableWebDebugging = it } ) } - if (lkmMode) { - UninstallItem(navigator) { - loadingDialog.withLoading(it) + } + + KsuIsValid { + Card( + modifier = Modifier + .padding(top = 12.dp) + .fillMaxWidth(), + ) { + val lkmMode = Natives.isLkmMode + if (lkmMode) { + val uninstall = stringResource(id = R.string.settings_uninstall) + SuperArrow( + title = uninstall, + leftAction = { + Icon( + Icons.Rounded.Delete, + modifier = Modifier.padding(end = 16.dp), + contentDescription = uninstall, + tint = colorScheme.onBackground, + ) + }, + onClick = { + showUninstallDialog.value = true + uninstallDialog + } + ) } } } - ) - // 关于卡片 - SettingsGroupCard( - title = stringResource(R.string.about), - content = { - SettingItem( - icon = Icons.Filled.Info, - title = stringResource(R.string.about), + Card( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + ) { + SuperArrow( + title = stringResource(id = R.string.send_log), + leftAction = { + Icon( + Icons.Rounded.BugReport, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.send_log), + tint = colorScheme.onBackground + ) + }, onClick = { - aboutDialog.show() + showSendLogDialog.value = true + sendLogDialog + }, + ) + val about = stringResource(id = R.string.about) + SuperArrow( + title = about, + leftAction = { + Icon( + Icons.Rounded.ContactPage, + modifier = Modifier.padding(end = 16.dp), + contentDescription = about, + tint = colorScheme.onBackground + ) + }, + onClick = { + navigator.navigate(AboutScreenDestination) { + launchSingleTop = true + } } ) } - ) - - Spacer(modifier = Modifier.height(SPACING_LARGE)) - } - } -} - -@Composable -private fun SettingsGroupCard( - title: String, - content: @Composable ColumnScope.() -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), - elevation = getCardElevation() - ) { - Column( - modifier = Modifier.padding(vertical = SPACING_MEDIUM) - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - color = MaterialTheme.colorScheme.primary - ) - content() - } - } -} - -@Composable -private fun WebUIEngineSelector( - selectedEngine: String, - onEngineSelected: (String) -> Unit -) { - var showDialog by remember { mutableStateOf(false) } - val engineOptions = listOf( - "default" to stringResource(R.string.engine_auto_select), - "wx" to stringResource(R.string.engine_force_webuix), - "ksu" to stringResource(R.string.engine_force_ksu) - ) - - SettingItem( - icon = Icons.Filled.WebAsset, - title = stringResource(R.string.use_webuix), - summary = engineOptions.find { it.first == selectedEngine }?.second - ?: stringResource(R.string.engine_auto_select), - onClick = { showDialog = true } - ) - - if (showDialog) { - AlertDialog( - onDismissRequest = { showDialog = false }, - title = { Text(stringResource(R.string.use_webuix)) }, - text = { - Column { - engineOptions.forEach { (value, label) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onEngineSelected(value) - showDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedEngine == value, - onClick = null - ) - Spacer(modifier = Modifier.width(SPACING_MEDIUM)) - Text(text = label) - } - } - } - }, - confirmButton = { - TextButton(onClick = { showDialog = false }) { - Text(stringResource(R.string.cancel)) - } - } - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun LogBottomSheet( - onDismiss: () -> Unit, - onSaveLog: () -> Unit, - onShareLog: () -> Unit -) { - ModalBottomSheet( - onDismissRequest = onDismiss, - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(SPACING_LARGE), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - LogActionButton( - icon = Icons.Filled.Save, - text = stringResource(R.string.save_log), - onClick = onSaveLog - ) - - LogActionButton( - icon = Icons.Filled.Share, - text = stringResource(R.string.send_log), - onClick = onShareLog - ) - } - Spacer(modifier = Modifier.height(SPACING_LARGE)) - } -} - -@Composable -fun LogActionButton( - icon: ImageVector, - text: String, - onClick: () -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clickable(onClick = onClick) - .padding(SPACING_MEDIUM) - ) { - 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(SPACING_MEDIUM)) - Text( - text = text, - style = MaterialTheme.typography.bodyMedium - ) - } -} - -@Composable -fun SettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = SPACING_LARGE, vertical = 12.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = SPACING_LARGE) - .size(24.dp) - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - if (summary != null) { - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium - ) - } - } - Icon( - imageVector = Icons.Filled.ChevronRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } -} - -@Composable -fun SwitchItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onCheckedChange(!checked) } - .padding(horizontal = SPACING_LARGE, vertical = 12.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(end = SPACING_LARGE) - .size(24.dp) - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - if (summary != null) { - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium - ) - } - } - Switch( - checked = checked, - onCheckedChange = onCheckedChange - ) - } -} - -@Composable -fun UninstallItem( - navigator: DestinationsNavigator, - withLoading: suspend (suspend () -> Unit) -> Unit -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val uninstallConfirmDialog = rememberConfirmDialog() - val showTodo = { - Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() - } - val uninstallDialog = rememberUninstallDialog { uninstallType -> - scope.launch { - val result = uninstallConfirmDialog.awaitConfirm( - title = context.getString(uninstallType.title), - content = context.getString(uninstallType.message) - ) - if (result == ConfirmResult.Confirmed) { - withLoading { - when (uninstallType) { - UninstallType.TEMPORARY -> showTodo() - UninstallType.PERMANENT -> navigator.navigate( - FlashScreenDestination(FlashIt.FlashUninstall) - ) - UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate( - FlashScreenDestination(FlashIt.FlashRestore) - ) - UninstallType.NONE -> Unit - } - } + Spacer(Modifier.height(bottomInnerPadding)) } } } - - SettingItem( - icon = Icons.Filled.Delete, - title = stringResource(id = R.string.settings_uninstall), - onClick = { - uninstallDialog.show() - } - ) } -enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) { +enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int) { TEMPORARY( + Icons.Rounded.RemoveModerator, R.string.settings_uninstall_temporary, - R.string.settings_uninstall_temporary_message, - Icons.Filled.Delete + R.string.settings_uninstall_temporary_message ), PERMANENT( + Icons.Rounded.DeleteForever, R.string.settings_uninstall_permanent, - R.string.settings_uninstall_permanent_message, - Icons.Filled.DeleteForever + R.string.settings_uninstall_permanent_message ), RESTORE_STOCK_IMAGE( + Icons.Rounded.RestartAlt, R.string.settings_restore_stock_image, - R.string.settings_restore_stock_image_message, - Icons.AutoMirrored.Filled.Undo + R.string.settings_restore_stock_image_message ), - NONE(0, 0, Icons.Filled.Delete) + NONE(Icons.Rounded.Adb, 0, 0) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val options = listOf( - UninstallType.PERMANENT, - UninstallType.RESTORE_STOCK_IMAGE - ) - val listOptions = options.map { - ListOption( - titleText = stringResource(it.title), - subtitleText = if (it.message != 0) stringResource(it.message) else null, - icon = IconSource(it.icon) - ) - } - - var selectedOption by remember { mutableStateOf(null) } - - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = MaterialTheme.colorScheme.surfaceContainerHigh - ) - ) { - AlertDialog( - onDismissRequest = { - dismiss() - }, - title = { - Text( - text = stringResource(R.string.settings_uninstall), - style = MaterialTheme.typography.headlineSmall, - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - options.forEachIndexed { index, option -> - val isSelected = selectedOption == option - val backgroundColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - Color.Transparent - val contentColor = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurface - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(backgroundColor) - .clickable { - selectedOption = option - } - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = option.icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = listOptions[index].titleText, - style = MaterialTheme.typography.titleMedium, - ) - listOptions[index].subtitleText?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = if (isSelected) - contentColor.copy(alpha = 0.8f) - else - MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - if (isSelected) { - Icon( - imageVector = Icons.Default.RadioButtonChecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } else { - Icon( - imageVector = Icons.Default.RadioButtonUnchecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = { - selectedOption?.let { onSelected(it) } - dismiss() - }, - enabled = selectedOption != null, - ) { - Text( - text = stringResource(android.R.string.ok) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - dismiss() - } - ) { - Text( - text = stringResource(android.R.string.cancel), - ) - } - }, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - TopAppBar( - title = { - Text( - text = stringResource(R.string.settings), - style = MaterialTheme.typography.titleLarge - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index d38d211a..05a25668 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -1,429 +1,375 @@ package com.sukisu.ultra.ui.screen -import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ApplicationInfo import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.* -import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically -import androidx.compose.animation.* +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +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.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.edit import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.LabelItemDefaults -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph +import com.kyant.capsule.ContinuousRoundedRectangle import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeSource +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import com.sukisu.ultra.Natives import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.FabMenuPresets -import com.sukisu.ultra.ui.component.SearchAppBar -import com.sukisu.ultra.ui.component.VerticalExpandableFab -import com.sukisu.ultra.ui.util.module.ModuleModify -import com.sukisu.ultra.ui.viewmodel.AppCategory -import com.sukisu.ultra.ui.viewmodel.SortType +import com.sukisu.ultra.ui.component.AppIconImage +import com.sukisu.ultra.ui.component.DropdownItem +import com.sukisu.ultra.ui.component.SearchBox +import com.sukisu.ultra.ui.component.SearchPager +import com.sukisu.ultra.ui.util.ownerNameForUid +import com.sukisu.ultra.ui.util.pickPrimary import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.basic.ArrowRight +import top.yukonga.miuix.kmp.icon.icons.useful.ImmersionMore +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic -enum class AppPriority(val value: Int) { - ROOT(1), CUSTOM(2), DEFAULT(3) -} - -data class BottomSheetMenuItem( - val icon: ImageVector, - val titleRes: Int, - val onClick: () -> Unit -) - -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable -fun SuperUserScreen(navigator: DestinationsNavigator) { +fun SuperUserPager( + navigator: DestinationsNavigator, + bottomInnerPadding: Dp +) { val viewModel = viewModel() val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val listState = rememberLazyListState() + val searchStatus by viewModel.searchStatus + val context = LocalContext.current - val snackBarHostState = remember { SnackbarHostState() } - - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - var showBottomSheet by remember { mutableStateOf(false) } - - val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState) - val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState) + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) LaunchedEffect(navigator) { - viewModel.search = "" - } - - LaunchedEffect(viewModel.selectedApps, viewModel.showBatchActions) { - if (viewModel.showBatchActions && viewModel.selectedApps.isEmpty()) { - viewModel.showBatchActions = false + if (viewModel.appList.value.isEmpty() || viewModel.searchResults.value.isEmpty()) { + viewModel.showSystemApps = prefs.getBoolean("show_system_apps", false) + viewModel.fetchAppList() } } - val filteredAndSortedAppGroups = remember( - viewModel.appGroupList, - viewModel.selectedCategory, - viewModel.currentSortType, - viewModel.search, - viewModel.showSystemApps - ) { - var groups = viewModel.appGroupList - - // 按分类筛选 - groups = when (viewModel.selectedCategory) { - AppCategory.ALL -> groups - AppCategory.ROOT -> groups.filter { it.allowSu } - AppCategory.CUSTOM -> groups.filter { !it.allowSu && it.hasCustomProfile } - AppCategory.DEFAULT -> groups.filter { !it.allowSu && !it.hasCustomProfile } - } - - // 排序 - groups.sortedWith { group1, group2 -> - val priority1 = when { - group1.allowSu -> AppPriority.ROOT - group1.hasCustomProfile -> AppPriority.CUSTOM - else -> AppPriority.DEFAULT - } - val priority2 = when { - group2.allowSu -> AppPriority.ROOT - group2.hasCustomProfile -> AppPriority.CUSTOM - else -> AppPriority.DEFAULT - } - - val priorityComparison = priority1.value.compareTo(priority2.value) - if (priorityComparison != 0) { - priorityComparison - } else { - when (viewModel.currentSortType) { - SortType.NAME_ASC -> group1.mainApp.label.lowercase() - .compareTo(group2.mainApp.label.lowercase()) - SortType.NAME_DESC -> group2.mainApp.label.lowercase() - .compareTo(group1.mainApp.label.lowercase()) - SortType.INSTALL_TIME_NEW -> group2.mainApp.packageInfo.firstInstallTime - .compareTo(group1.mainApp.packageInfo.firstInstallTime) - SortType.INSTALL_TIME_OLD -> group1.mainApp.packageInfo.firstInstallTime - .compareTo(group2.mainApp.packageInfo.firstInstallTime) - else -> group1.mainApp.label.lowercase() - .compareTo(group2.mainApp.label.lowercase()) - } - } - } + LaunchedEffect(searchStatus.searchText) { + viewModel.updateSearchText(searchStatus.searchText) } - val appCounts = remember(viewModel.appGroupList, viewModel.showSystemApps) { - mapOf( - AppCategory.ALL to viewModel.appGroupList.size, - AppCategory.ROOT to viewModel.appGroupList.count { it.allowSu }, - AppCategory.CUSTOM to viewModel.appGroupList.count { !it.allowSu && it.hasCustomProfile }, - AppCategory.DEFAULT to viewModel.appGroupList.count { !it.allowSu && !it.hasCustomProfile } - ) + val scrollBehavior = MiuixScrollBehavior() + val listState = rememberLazyListState() + val dynamicTopPadding by remember { + derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) } } + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) Scaffold( topBar = { - SearchAppBar( - title = { TopBarTitle(viewModel.selectedCategory, appCounts) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - dropdownContent = { - IconButton(onClick = { showBottomSheet = true }) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(id = R.string.settings), - ) - } - }, - scrollBehavior = scrollBehavior - ) - }, - snackbarHost = { SnackbarHost(snackBarHostState) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - floatingActionButton = { - SuperUserFab(viewModel, filteredAndSortedAppGroups, listState, scope) - } - ) { innerPadding -> - SuperUserContent( - innerPadding = innerPadding, - viewModel = viewModel, - filteredAndSortedAppGroups = filteredAndSortedAppGroups, - listState = listState, - scrollBehavior = scrollBehavior, - navigator = navigator, - scope = scope - ) - - if (showBottomSheet) { - SuperUserBottomSheet( - bottomSheetState = bottomSheetState, - onDismiss = { showBottomSheet = false }, - viewModel = viewModel, - appCounts = appCounts, - backupLauncher = backupLauncher, - restoreLauncher = restoreLauncher, - scope = scope, - listState = listState - ) - } - } -} - -@Composable -private fun TopBarTitle( - selectedCategory: AppCategory, - appCounts: Map -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(stringResource(R.string.superuser)) - - if (selectedCategory != AppCategory.ALL) { - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier.padding(start = 4.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(selectedCategory.displayNameRes), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - Text( - text = "(${appCounts[selectedCategory] ?: 0})", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - } -} - -@Composable -private fun SuperUserFab( - viewModel: SuperUserViewModel, - filteredAndSortedAppGroups: List, - listState: androidx.compose.foundation.lazy.LazyListState, - scope: CoroutineScope -) { - VerticalExpandableFab( - menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - FabMenuPresets.getBatchActionMenuItems( - onCancel = { - viewModel.selectedApps = emptySet() - viewModel.showBatchActions = false - }, - onDeny = { scope.launch { viewModel.updateBatchPermissions(false) } }, - onAllow = { scope.launch { viewModel.updateBatchPermissions(true) } }, - onUnmountModules = { - scope.launch { viewModel.updateBatchPermissions( - allowSu = false, - umountModules = true - ) } - }, - onDisableUnmount = { - scope.launch { viewModel.updateBatchPermissions( - allowSu = false, - umountModules = false - ) } - } - ) - } else { - FabMenuPresets.getScrollMenuItems( - onScrollToTop = { scope.launch { listState.animateScrollToItem(0) } }, - onScrollToBottom = { - scope.launch { - val lastIndex = filteredAndSortedAppGroups.size - 1 - if (lastIndex >= 0) listState.animateScrollToItem(lastIndex) - } - } - ) - }, - mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - Icons.Filled.GridView - } else { - Icons.Filled.Add - }, - mainButtonExpandedIcon = Icons.Filled.Close - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SuperUserContent( - innerPadding: PaddingValues, - viewModel: SuperUserViewModel, - filteredAndSortedAppGroups: List, - listState: androidx.compose.foundation.lazy.LazyListState, - scrollBehavior: TopAppBarScrollBehavior, - navigator: DestinationsNavigator, - scope: CoroutineScope -) { - val expandedGroups = remember { mutableStateOf(setOf()) } - val density = LocalDensity.current - val targetSizePx = remember(density) { with(density) { 36.dp.roundToPx() } } - val context = LocalContext.current - - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { scope.launch { viewModel.fetchAppList() } }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - ) { - filteredAndSortedAppGroups.forEachIndexed { _, appGroup -> - item(key = "${appGroup.uid}-${appGroup.mainApp.packageName}") { - AppGroupItem( - expandedGroups = expandedGroups, - appGroup = appGroup, - isSelected = appGroup.packageNames.any { viewModel.selectedApps.contains(it) }, - onToggleSelection = { - appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } - }, - onClick = { - if (viewModel.showBatchActions) { - appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } - } else if (appGroup.apps.size > 1) { - expandedGroups.value = if (expandedGroups.value.contains(appGroup.uid)) { - expandedGroups.value - appGroup.uid - } else { - expandedGroups.value + appGroup.uid - } - } else { - navigator.navigate(AppProfileScreenDestination(appGroup.mainApp)) + searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) { + TopAppBar( + color = Color.Transparent, + title = stringResource(R.string.superuser), + actions = { + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false } - }, - onLongClick = { - if (!viewModel.showBatchActions) { - viewModel.toggleBatchMode() - appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } + ) { + ListPopupColumn { + DropdownItem( + text = if (viewModel.showSystemApps) { + stringResource(R.string.hide_system_apps) + } else { + stringResource(R.string.show_system_apps) + }, + optionSize = 1, + onSelectedIndexChange = { + viewModel.showSystemApps = !viewModel.showSystemApps + prefs.edit { + putBoolean("show_system_apps", viewModel.showSystemApps) + } + scope.launch { + viewModel.fetchAppList() + } + showTopPopup.value = false + }, + index = 0 + ) } - }, - viewModel = viewModel - ) - } - - if (appGroup.apps.size <= 1) return@forEachIndexed - - items(appGroup.apps, key = { "${it.packageName}-${it.uid}" }) { app -> - val painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(context) - .data(app.packageInfo) - .size(targetSizePx) - .crossfade(true) - .build() - ) - - val listItemContent = remember(app.packageName, appGroup.uid) { - @Composable { - ListItem( - modifier = Modifier - .clickable { navigator.navigate(AppProfileScreenDestination(app)) } - .fillMaxWidth() - .padding(start = 10.dp), - headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, - supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, - leadingContent = { - Image( - painter = painter, - contentDescription = app.label, - modifier = Modifier - .padding(4.dp) - .size(36.dp), - contentScale = ContentScale.Crop - ) - } + } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { + showTopPopup.value = true + }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.ImmersionMore, + tint = colorScheme.onSurface, + contentDescription = stringResource(id = R.string.settings) ) } - } - + }, + scrollBehavior = scrollBehavior + ) + } + }, + popupHost = { + val allGroups = remember(viewModel.appList.value) { buildGroups(viewModel.appList.value) } + val matchedByUid = remember(viewModel.searchResults.value) { + viewModel.searchResults.value.groupBy { it.uid } + } + val searchGroups = remember(allGroups, matchedByUid) { + allGroups.filter { matchedByUid.containsKey(it.uid) } + } + val expandedSearchUids = remember { mutableStateOf(setOf()) } + LaunchedEffect(matchedByUid) { + expandedSearchUids.value = searchGroups + .filter { it.apps.size > 1 } + .map { it.uid } + .toSet() + } + searchStatus.SearchPager( + defaultResult = {}, + searchBarTopPadding = dynamicTopPadding, + ) { + items(searchGroups, key = { it.uid }) { group -> + val expanded = expandedSearchUids.value.contains(group.uid) AnimatedVisibility( - visible = expandedGroups.value.contains(appGroup.uid), + visible = searchGroups.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { - listItemContent() + Column( + Modifier.padding(top = 6.dp) + ) { + GroupItem( + group = group, + onToggleExpand = { + if (group.apps.size > 1) { + expandedSearchUids.value = + if (expanded) expandedSearchUids.value - group.uid else expandedSearchUids.value + group.uid + } + }, + ) { + navigator.navigate(AppProfileScreenDestination(group.primary)) { + launchSingleTop = true + } + } + AnimatedVisibility( + visible = expanded && group.apps.size > 1, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column { + val matchedApps = matchedByUid[group.uid] ?: emptyList() + matchedApps.forEach { app -> SimpleAppItem(app) } + Spacer(Modifier.height(6.dp)) + } + } + } } } - } - - if (filteredAndSortedAppGroups.isEmpty()) { item { - Box( - modifier = Modifier.fillMaxWidth().height(400.dp), - contentAlignment = Alignment.Center + val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() + Spacer(Modifier.height(maxOf(bottomInnerPadding, imeBottomPadding))) + } + } + }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + searchStatus.SearchBox( + searchBarTopPadding = dynamicTopPadding, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + hazeState = hazeState, + hazeStyle = hazeStyle + ) { boxHeight -> + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) + viewModel.fetchAppList() + isRefreshing = false + } + } + val refreshTexts = listOf( + stringResource(R.string.refresh_pulling), + stringResource(R.string.refresh_release), + stringResource(R.string.refresh_refresh), + stringResource(R.string.refresh_complete), + ) + if (viewModel.appList.value.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection), + bottom = bottomInnerPadding + ), + contentAlignment = Alignment.Center + ) { + Text( + text = if (viewModel.isRefreshing) "Loading..." else "Empty", + textAlign = TextAlign.Center, + color = Color.Gray, + ) + } + } else { + val allGroups = remember(SuperUserViewModel.apps) { buildGroups(SuperUserViewModel.apps) } + val visibleUidSet = remember(viewModel.appList.value) { viewModel.appList.value.map { it.uid }.toSet() } + val expandedUids = remember { mutableStateOf(setOf()) } + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + ) { + LazyColumn( + state = listState, + modifier = Modifier + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .hazeSource(state = hazeState), + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), + overscrollEffect = null, ) { - if ((viewModel.isRefreshing || viewModel.appGroupList.isEmpty()) && viewModel.search.isEmpty()) { - LoadingAnimation(isLoading = true) - } else { - EmptyState( - selectedCategory = viewModel.selectedCategory, - isSearchEmpty = viewModel.search.isNotEmpty() - ) + items(allGroups, key = { it.uid }) { group -> + val expanded = expandedUids.value.contains(group.uid) + val isVisible = visibleUidSet.contains(group.uid) + AnimatedVisibility( + visible = isVisible, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column { + GroupItem( + group = group, + onToggleExpand = { + if (group.apps.size > 1) { + expandedUids.value = + if (expanded) expandedUids.value - group.uid else expandedUids.value + group.uid + } + } + ) { + navigator.navigate(AppProfileScreenDestination(group.primary)) { + launchSingleTop = true + } + } + AnimatedVisibility( + visible = expanded && group.apps.size > 1, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column { + group.apps.forEach { app -> + SimpleAppItem(app) + } + Spacer(Modifier.height(6.dp)) + } + } + } + } + } + item { + Spacer(Modifier.height(bottomInnerPadding)) } } } @@ -432,530 +378,219 @@ private fun SuperUserContent( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun SuperUserBottomSheet( - bottomSheetState: SheetState, - onDismiss: () -> Unit, - viewModel: SuperUserViewModel, - appCounts: Map, - backupLauncher: androidx.activity.result.ActivityResultLauncher, - restoreLauncher: androidx.activity.result.ActivityResultLauncher, - scope: CoroutineScope, - listState: androidx.compose.foundation.lazy.LazyListState +private fun SimpleAppItem( + app: SuperUserViewModel.AppInfo, ) { - val bottomSheetMenuItems = remember(viewModel.showSystemApps) { - listOf( - BottomSheetMenuItem( - icon = Icons.Filled.Refresh, - titleRes = R.string.refresh, - onClick = { - scope.launch { - viewModel.fetchAppList() - bottomSheetState.hide() - onDismiss() - } - } - ), - BottomSheetMenuItem( - icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, - titleRes = if (viewModel.showSystemApps) R.string.hide_system_apps else R.string.show_system_apps, - onClick = { - viewModel.updateShowSystemApps(!viewModel.showSystemApps) - scope.launch { - kotlinx.coroutines.delay(100) - bottomSheetState.hide() - onDismiss() - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.Save, - titleRes = R.string.backup_allowlist, - onClick = { - backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) - scope.launch { - bottomSheetState.hide() - onDismiss() - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.RestoreFromTrash, - titleRes = R.string.restore_allowlist, - onClick = { - restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) - scope.launch { - bottomSheetState.hide() - onDismiss() - } - } + Row { + Box( + modifier = Modifier + .padding(start = 12.dp) + .width(6.dp) + .height(24.dp) + .align(Alignment.CenterVertically) + .clip(ContinuousRoundedRectangle(16.dp)) + .background(colorScheme.primaryContainer) + ) + Card( + modifier = Modifier + .padding(start = 6.dp, end = 12.dp, bottom = 6.dp) + ) { + BasicComponent( + title = app.label, + summary = app.packageName, + leftAction = { + AppIconImage( + packageInfo = app.packageInfo, + label = app.label, + modifier = Modifier + .padding(end = 9.dp) + .size(40.dp) + ) + }, + insideMargin = PaddingValues(horizontal = 9.dp) ) - ) - } - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = bottomSheetState, - dragHandle = { - Surface( - modifier = Modifier.padding(vertical = 11.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp) - ) { - Box(Modifier.size(width = 32.dp, height = 4.dp)) - } - } - ) { - BottomSheetContent( - menuItems = bottomSheetMenuItems, - currentSortType = viewModel.currentSortType, - onSortTypeChanged = { newSortType -> - viewModel.updateCurrentSortType(newSortType) - scope.launch { - bottomSheetState.hide() - onDismiss() - } - }, - selectedCategory = viewModel.selectedCategory, - onCategorySelected = { newCategory -> - viewModel.updateSelectedCategory(newCategory) - scope.launch { - listState.animateScrollToItem(0) - bottomSheetState.hide() - onDismiss() - } - }, - appCounts = appCounts - ) - } -} - -@Composable -private fun BottomSheetContent( - menuItems: List, - currentSortType: SortType, - onSortTypeChanged: (SortType) -> Unit, - selectedCategory: AppCategory, - onCategorySelected: (AppCategory) -> Unit, - appCounts: Map -) { - Column( - modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp) - ) { - Text( - text = stringResource(R.string.menu_options), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) - - LazyVerticalGrid( - columns = GridCells.Fixed(4), - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(menuItems) { menuItem -> - BottomSheetMenuItemView(menuItem = menuItem) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) - - Text( - text = stringResource(R.string.sort_options), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) - - LazyRow( - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(SortType.entries.toTypedArray()) { sortType -> - FilterChip( - onClick = { onSortTypeChanged(sortType) }, - label = { Text(stringResource(sortType.displayNameRes)) }, - selected = currentSortType == sortType - ) - } - } - - Spacer(modifier = Modifier.height(24.dp)) - HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) - - Text( - text = stringResource(R.string.app_categories), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) - ) - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(AppCategory.entries.toTypedArray()) { category -> - CategoryChip( - category = category, - isSelected = selectedCategory == category, - onClick = { onCategorySelected(category) }, - appCount = appCounts[category] ?: 0 - ) - } } } } +@Immutable +private data class GroupedApps( + val uid: Int, + val apps: List, + val primary: SuperUserViewModel.AppInfo, + val anyAllowSu: Boolean, + val anyCustom: Boolean, +) + +private fun buildGroups(apps: List): List { + val comparator = compareBy { + when { + it.allowSu -> 0 + it.hasCustomProfile -> 1 + else -> 2 + } + }.thenBy { it.label.lowercase() } + val groups = apps.groupBy { it.uid }.map { (uid, list) -> + val sorted = list.sortedWith(comparator) + val primary = pickPrimary(sorted) + GroupedApps( + uid = uid, + apps = sorted, + primary = primary, + anyAllowSu = sorted.any { it.allowSu }, + anyCustom = sorted.any { it.hasCustomProfile }, + ) + } + return groups.sortedWith(Comparator { a, b -> + fun rank(g: GroupedApps): Int = when { + g.anyAllowSu -> 0 + g.anyCustom -> 1 + g.apps.size > 1 -> 2 + Natives.uidShouldUmount(g.uid) -> 4 + else -> 3 + } + + val ra = rank(a) + val rb = rank(b) + if (ra != rb) return@Comparator ra - rb + return@Comparator when (ra) { + 2 -> a.uid.compareTo(b.uid) + else -> a.primary.label.lowercase().compareTo(b.primary.label.lowercase()) + } + }) +} + @Composable -private fun CategoryChip( - category: AppCategory, - isSelected: Boolean, - onClick: () -> Unit, - appCount: Int, - modifier: Modifier = Modifier +private fun GroupItem( + group: GroupedApps, + onToggleExpand: () -> Unit, + onClickPrimary: () -> Unit, ) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1.0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "categoryChipScale" - ) - - Surface( - modifier = modifier - .fillMaxWidth() - .scale(scale) - .clickable(interactionSource = interactionSource, indication = null) { onClick() }, - shape = RoundedCornerShape(12.dp), - color = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant - }, - tonalElevation = if (isSelected) 4.dp else 0.dp + val isDark = isSystemInDarkTheme() + val colorScheme = colorScheme + val bg = remember { colorScheme.secondaryContainer.copy(alpha = 0.8f) } + val rootBg = remember { colorScheme.tertiaryContainer.copy(alpha = 0.6f) } + val unmountBg = remember(isDark) { if (isDark) Color.White.copy(alpha = 0.4f) else Color.Black.copy(alpha = 0.3f) } + val fg = remember { colorScheme.onSecondaryContainer } + val rootFg = remember { colorScheme.onTertiaryContainer.copy(alpha = 0.8f) } + val unmountFg = remember(isDark) { if (isDark) Color.Black.copy(alpha = 0.4f) else Color.White.copy(alpha = 0.8f) } + val userId = group.uid / 100000 + val packageInfo = group.primary.packageInfo + val applicationInfo = packageInfo.applicationInfo + val hasSharedUserId = !packageInfo.sharedUserId.isNullOrEmpty() + val isSystemApp = applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0 + || applicationInfo.flags.and(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 + val tags = remember(group.uid, group.anyAllowSu, group.anyCustom) { + buildList { + if (group.anyAllowSu) add(StatusMeta("ROOT", rootBg, rootFg)) + if (Natives.uidShouldUmount(group.uid)) add(StatusMeta("UMOUNT", unmountBg, unmountFg)) + if (group.anyCustom) add(StatusMeta("CUSTOM", bg, fg)) + if (userId != 0) add(StatusMeta("USER $userId", bg, fg)) + if (isSystemApp) add(StatusMeta("SYSTEM", bg, fg)) + if (hasSharedUserId) add(StatusMeta("SHARED UID", bg, fg)) + } + } + Card( + modifier = Modifier + .padding(horizontal = 12.dp) + .padding(bottom = 12.dp), + onClick = onClickPrimary, + onLongPress = if (group.apps.size > 1) onToggleExpand else null, + pressFeedbackType = PressFeedbackType.Sink, + showIndication = true, + insideMargin = PaddingValues(vertical = 8.dp, horizontal = 16.dp) ) { - Column( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) + Row( + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + AppIconImage( + packageInfo = group.primary.packageInfo, + label = group.primary.label, + modifier = Modifier + .padding(end = 12.dp) + .size(46.dp) + ) + Column( + modifier = Modifier + .weight(1f), ) { Text( - text = stringResource(category.displayNameRes), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium - ), - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, + text = if (group.apps.size > 1) ownerNameForUid(group.uid) else group.primary.label, + modifier = Modifier.basicMarquee(), + fontWeight = FontWeight(550), + color = colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis + softWrap = false ) - - AnimatedVisibility( - visible = isSelected, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = stringResource(R.string.selected), - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(16.dp) - ) - } - } - - Text( - text = "$appCount apps", - style = MaterialTheme.typography.labelSmall, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } - } -} - -@Composable -private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1.0f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "menuItemScale" - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .scale(scale) - .clickable(interactionSource = interactionSource, indication = null) { menuItem.onClick() } - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Surface( - modifier = Modifier.size(48.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) { - Box(contentAlignment = Alignment.Center) { - Icon( - imageVector = menuItem.icon, - contentDescription = stringResource(menuItem.titleRes), - modifier = Modifier.size(24.dp) - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = stringResource(menuItem.titleRes), - style = MaterialTheme.typography.labelSmall, - textAlign = TextAlign.Center, - maxLines = 2 - ) - } -} - -@Composable -private fun LoadingAnimation( - modifier: Modifier = Modifier, - isLoading: Boolean = true -) { - val infiniteTransition = rememberInfiniteTransition(label = "loading") - - val alpha by infiniteTransition.animateFloat( - initialValue = 0.3f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(600, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "alpha" - ) - - AnimatedVisibility( - visible = isLoading, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), - modifier = modifier - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - LinearProgressIndicator( - modifier = Modifier.width(200.dp).height(4.dp), - color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) - ) - } - } -} - -@Composable -@SuppressLint("ModifierParameter") -private fun EmptyState( - selectedCategory: AppCategory, - modifier: Modifier = Modifier, - isSearchEmpty: Boolean = false -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier - ) { - Icon( - imageVector = if (isSearchEmpty) Icons.Filled.SearchOff else Icons.Filled.Archive, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier.size(96.dp).padding(bottom = 16.dp) - ) - Text( - text = if (isSearchEmpty || selectedCategory == AppCategory.ALL) { - stringResource(R.string.no_apps_found) - } else { - stringResource(R.string.no_apps_in_category) - }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - } -} - -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) -@Composable -private fun AppGroupItem( - appGroup: SuperUserViewModel.AppGroup, - isSelected: Boolean, - onToggleSelection: () -> Unit, - onClick: () -> Unit, - onLongClick: () -> Unit, - viewModel: SuperUserViewModel, - expandedGroups: MutableState> -) { - val mainApp = appGroup.mainApp - - ListItem( - modifier = Modifier.pointerInput(Unit) { - detectTapGestures( - onLongPress = { onLongClick() }, - onTap = { onClick() } - ) - }, - headlineContent = { - Text(mainApp.label) - }, - supportingContent = { - Column { - val summaryText = if (appGroup.apps.size > 1) { - stringResource(R.string.group_contains_apps, appGroup.apps.size) - } else { - mainApp.packageName - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(summaryText) - - if (appGroup.apps.size > 1) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.rotate( - animateFloatAsState( - targetValue = if (expandedGroups.value.contains(appGroup.uid)) 180f else 0f, - animationSpec = tween(200, easing = LinearOutSlowInEasing), - label = "" - ).value - ) - ) - } - } - - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - if (appGroup.allowSu) { - LabelItem(text = "ROOT") + Text( + text = if (group.apps.size > 1) { + stringResource(R.string.group_contains_apps, group.apps.size) } else { - if (Natives.uidShouldUmount(appGroup.uid)) { - LabelItem( - text = "UMOUNT", - style = LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) - } - } - if (appGroup.hasCustomProfile) { - LabelItem( - text = "CUSTOM", - style = LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ) - ) - } else if (!appGroup.allowSu) { - LabelItem( - text = "DEFAULT", - style = LabelItemDefaults.style.copy( - containerColor = Color.Gray - ) - ) - } - if (appGroup.apps.size > 1) { - appGroup.userName?.let { - LabelItem( - text = it, - style = LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ) - ) - } - } - } - } - }, - leadingContent = { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(mainApp.packageInfo) - .crossfade(true) - .build(), - contentDescription = mainApp.label, - modifier = Modifier.padding(4.dp).width(48.dp).height(48.dp) - ) - }, - trailingContent = { - AnimatedVisibility( - visible = viewModel.showBatchActions, - enter = fadeIn(animationSpec = tween(200)) + scaleIn( - animationSpec = tween(200), - initialScale = 0.6f - ), - exit = fadeOut(animationSpec = tween(200)) + scaleOut( - animationSpec = tween(200), - targetScale = 0.6f + group.primary.packageName + }, + modifier = Modifier + .basicMarquee(), + fontSize = 12.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurfaceVariantSummary, + maxLines = 1, + softWrap = false ) - ) { - val checkboxInteractionSource = remember { MutableInteractionSource() } - val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState() - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End + FlowRow( + modifier = Modifier.padding(top = 3.dp, bottom = 3.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - AnimatedVisibility( - visible = isCheckboxPressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - text = if (isSelected) stringResource(R.string.selected) else stringResource(R.string.select), - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(end = 4.dp) + tags.forEach { tag -> + StatusTag( + label = tag.label, + backgroundColor = tag.bg, + contentColor = tag.fg ) } - Checkbox( - checked = isSelected, - onCheckedChange = { onToggleSelection() }, - interactionSource = checkboxInteractionSource, - ) } } + Image( + modifier = Modifier + .padding(start = 8.dp) + .size(width = 10.dp, height = 16.dp), + imageVector = MiuixIcons.Basic.ArrowRight, + contentDescription = null, + colorFilter = ColorFilter.tint(colorScheme.onSurfaceVariantActions), + ) } - ) -} \ No newline at end of file + } +} + +@Composable +fun StatusTag( + label: String, + backgroundColor: Color, + contentColor: Color +) { + Box( + modifier = Modifier + .background( + color = backgroundColor, + shape = ContinuousRoundedRectangle(6.dp) + ) + ) { + Text( + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), + text = label, + color = contentColor, + fontSize = 9.sp, + fontWeight = FontWeight(750), + maxLines = 1, + softWrap = false + ) + } +} + +@Immutable +private data class StatusMeta( + val label: String, + val bg: Color, + val fg: Color +) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt index 6aa1defa..f283ae83 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt @@ -1,32 +1,65 @@ package com.sukisu.ultra.ui.screen -import android.content.ClipData -import android.content.ClipboardManager import android.widget.Toast -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +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.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ImportExport -import androidx.compose.material.icons.filled.Sync -import androidx.compose.material3.* -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* +import androidx.compose.material.icons.outlined.Fingerprint +import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.Shield +import androidx.compose.material.icons.rounded.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.getSystemService import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination @@ -35,27 +68,57 @@ import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScr import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.getOr -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.viewmodel.TemplateViewModel +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.DropdownItem +import com.sukisu.ultra.ui.viewmodel.TemplateViewModel +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.HorizontalDivider +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupColumn +import top.yukonga.miuix.kmp.basic.ListPopupDefaults +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.PullToRefresh +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Copy +import top.yukonga.miuix.kmp.icon.icons.useful.Refresh +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ - -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun AppProfileTemplateScreen( navigator: DestinationsNavigator, resultRecipient: ResultRecipient ) { val viewModel = viewModel() val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() LaunchedEffect(Unit) { if (viewModel.templateList.isEmpty()) { @@ -70,10 +133,45 @@ fun AppProfileTemplateScreen( } } + val listState = rememberLazyListState() + var fabVisible by remember { mutableStateOf(true) } + var scrollDistance by remember { mutableFloatStateOf(0f) } + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val isScrolledToEnd = + (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 + && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size + ?: 0) < listState.layoutInfo.viewportEndOffset) + val delta = available.y + if (!isScrolledToEnd) { + scrollDistance += delta + if (scrollDistance < -50f) { + if (fabVisible) fabVisible = false + scrollDistance = 0f + } else if (scrollDistance > 50f) { + if (!fabVisible) fabVisible = true + scrollDistance = 0f + } + } + return Offset.Zero + } + } + } + val offsetHeight by animateDpAsState( + targetValue = if (fabVisible) 0.dp else 100.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), + animationSpec = tween(durationMillis = 350) + ) + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) + Scaffold( topBar = { + val clipboardManager = LocalClipboardManager.current val context = LocalContext.current - val clipboardManager = context.getSystemService() val showToast = fun(msg: String) { scope.launch(Dispatchers.Main) { Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() @@ -85,20 +183,20 @@ fun AppProfileTemplateScreen( scope.launch { viewModel.fetchTemplates(true) } }, onImport = { - scope.launch { - val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString() - if (clipboardText.isNullOrEmpty()) { + clipboardManager.getText()?.text?.let { + if (it.isEmpty()) { showToast(context.getString(R.string.app_profile_template_import_empty)) - return@launch + return@let + } + scope.launch { + viewModel.importTemplates( + it, { + showToast(context.getString(R.string.app_profile_template_import_success)) + viewModel.fetchTemplates(false) + }, + showToast + ) } - viewModel.importTemplates( - clipboardText, - { - showToast(context.getString(R.string.app_profile_template_import_success)) - viewModel.fetchTemplates(false) - }, - showToast - ) } }, onExport = { @@ -107,176 +205,300 @@ fun AppProfileTemplateScreen( { showToast(context.getString(R.string.app_profile_template_export_empty)) } - ) { text -> - clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text)) + ) { + clipboardManager.setText(AnnotatedString(it)) } } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, floatingActionButton = { - ExtendedFloatingActionButton( + FloatingActionButton( + containerColor = colorScheme.primary, + shadowElevation = 0.dp, onClick = { - navigator.navigate( - TemplateEditorScreenDestination( - TemplateViewModel.TemplateInfo(), - false - ) + navigator.navigate(TemplateEditorScreenDestination(TemplateViewModel.TemplateInfo(), false)) { + launchSingleTop = true + } + }, + modifier = Modifier + .offset(y = offsetHeight) + .padding( + bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, + end = 20.dp + ) + .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), + content = { + Icon( + Icons.Rounded.Add, + null, + Modifier.size(40.dp), + tint = Color.White ) }, - icon = { Icon(Icons.Filled.Add, null) }, - text = { Text(stringResource(id = R.string.app_profile_template_create)) }, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - isRefreshing = viewModel.isRefreshing, - onRefresh = { - scope.launch { viewModel.fetchTemplates() } + var isRefreshing by rememberSaveable { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + delay(350) + viewModel.fetchTemplates() + isRefreshing = false } + } + val refreshTexts = listOf( + stringResource(R.string.refresh_pulling), + stringResource(R.string.refresh_release), + stringResource(R.string.refresh_refresh), + stringResource(R.string.refresh_complete), + ) + val layoutDirection = LocalLayoutDirection.current + PullToRefresh( + isRefreshing = isRefreshing, + pullToRefreshState = pullToRefreshState, + onRefresh = { isRefreshing = true }, + refreshTexts = refreshTexts, + contentPadding = PaddingValues( + top = innerPadding.calculateTopPadding() + 12.dp, + start = innerPadding.calculateStartPadding(layoutDirection), + end = innerPadding.calculateEndPadding(layoutDirection) + ), ) { LazyColumn( modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - contentPadding = remember { - PaddingValues(bottom = 16.dp + 56.dp + 16.dp /* Scaffold Fab Spacing + Fab container height */) - } + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(nestedScrollConnection) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .hazeSource(state = hazeState) + .padding(horizontal = 12.dp), + contentPadding = innerPadding, + overscrollEffect = null ) { + item { + Spacer(Modifier.height(12.dp)) + } items(viewModel.templateList, key = { it.id }) { app -> TemplateItem(navigator, app) } + item { + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } } } } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun TemplateItem( navigator: DestinationsNavigator, template: TemplateViewModel.TemplateInfo ) { - ListItem( - modifier = Modifier - .clickable { - navigator.navigate(TemplateEditorScreenDestination(template, !template.local)) - }, - headlineContent = { Text(template.name) }, - supportingContent = { - Column { + Card( + modifier = Modifier.padding(bottom = 12.dp), + onClick = { + navigator.navigate(TemplateEditorScreenDestination(template, !template.local)) { + popUpTo(TemplateEditorScreenDestination) { + inclusive = true + } + launchSingleTop = true + } + }, + showIndication = true, + pressFeedbackType = PressFeedbackType.Sink + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = "${template.id}${if (template.author.isEmpty()) "" else "@${template.author}"}", - style = MaterialTheme.typography.bodySmall, - fontSize = MaterialTheme.typography.bodySmall.fontSize, + text = template.name, + fontWeight = FontWeight(550), + color = colorScheme.onSurface, ) - Text(template.description) - FlowRow { - LabelText(label = "UID: ${template.uid}") - LabelText(label = "GID: ${template.gid}") - LabelText(label = template.context) - if (template.local) { - LabelText(label = "local") - } else { - LabelText(label = "remote") - } + Spacer(modifier = Modifier.weight(1f)) + if (template.local) { + Text( + text = "LOCAL", + color = colorScheme.onTertiaryContainer, + fontWeight = FontWeight(750), + style = MiuixTheme.textStyles.footnote1 + ) + } else { + Text( + text = "REMOTE", + color = colorScheme.onSurfaceSecondary, + fontWeight = FontWeight(750), + style = MiuixTheme.textStyles.footnote1 + ) } } + + Text( + text = "${template.id}${if (template.author.isEmpty()) "" else " by @${template.author}"}", + modifier = Modifier.padding(top = 1.dp), + fontSize = 12.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurfaceVariantSummary, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = template.description, + fontSize = 14.sp, + color = colorScheme.onSurfaceVariantSummary, + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = 0.5.dp, + color = colorScheme.outline.copy(alpha = 0.5f) + ) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + InfoChip( + icon = Icons.Outlined.Fingerprint, + text = "UID: ${template.uid}" + ) + InfoChip( + icon = Icons.Outlined.Group, + text = "GID: ${template.gid}" + ) + InfoChip( + icon = Icons.Outlined.Shield, + text = template.context + ) + } } - ) + } +} + +@Composable +private fun InfoChip(icon: ImageVector, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = colorScheme.onSurfaceSecondary.copy(alpha = 0.8f) + ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = text, + fontSize = 12.sp, + fontWeight = FontWeight(550), + color = colorScheme.onSurfaceSecondary + ) + } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( onBack: () -> Unit, onSync: () -> Unit = {}, onImport: () -> Unit = {}, onExport: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, + hazeState: HazeState, + hazeStyle: HazeStyle, ) { - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - val cardAlpha = CardConfig.cardAlpha - TopAppBar( - title = { - Text(stringResource(R.string.settings_profile_template)) + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), + color = Color.Transparent, + title = stringResource(R.string.settings_profile_template), navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + ) { + Icon( + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } }, actions = { - IconButton(onClick = onSync) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSync + ) { Icon( - Icons.Filled.Sync, - contentDescription = stringResource(id = R.string.app_profile_template_sync) + imageVector = MiuixIcons.Useful.Refresh, + contentDescription = stringResource(id = R.string.app_profile_template_sync), + tint = colorScheme.onBackground ) } - var showDropdown by remember { mutableStateOf(false) } - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Filled.ImportExport, - contentDescription = stringResource(id = R.string.app_profile_import_export) - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem(text = { - Text(stringResource(id = R.string.app_profile_import_from_clipboard)) - }, onClick = { - onImport() - showDropdown = false - }) - DropdownMenuItem(text = { - Text(stringResource(id = R.string.app_profile_export_to_clipboard)) - }, onClick = { - onExport() - showDropdown = false - }) + val showTopPopup = remember { mutableStateOf(false) } + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + showTopPopup.value = false + } + ) { + ListPopupColumn { + val items = listOf( + stringResource(id = R.string.app_profile_import_from_clipboard), + stringResource(id = R.string.app_profile_export_to_clipboard) + ) + items.forEachIndexed { index, text -> + DropdownItem( + text = text, + optionSize = items.size, + index = index, + onSelectedIndexChange = { selectedIndex -> + if (selectedIndex == 0) { + onImport() + } else { + onExport() + } + showTopPopup.value = false + } + ) + } } } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { showTopPopup.value = true }, + holdDownState = showTopPopup.value + ) { + Icon( + imageVector = MiuixIcons.Useful.Copy, + contentDescription = stringResource(id = R.string.app_profile_import_export), + tint = colorScheme.onBackground + ) + } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } - -@Composable -fun LabelText(label: String) { - Box( - modifier = Modifier - .padding(top = 4.dp, end = 4.dp) - .background( - Color.Black, - shape = RoundedCornerShape(4.dp) - ) - ) { - Text( - text = label, - modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), - style = TextStyle( - fontSize = 8.sp, - color = Color.White, - ) - ) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt index 89e3e7b4..768659df 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt @@ -2,47 +2,75 @@ package com.sukisu.ultra.ui.screen import android.widget.Toast import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions -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.DeleteForever -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.result.ResultBackNavigator +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource import com.sukisu.ultra.Natives import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.EditText import com.sukisu.ultra.ui.component.profile.RootProfileConfig import com.sukisu.ultra.ui.util.deleteAppProfileTemplate import com.sukisu.ultra.ui.util.getAppProfileTemplate import com.sukisu.ultra.ui.util.setAppProfileTemplate import com.sukisu.ultra.ui.viewmodel.TemplateViewModel import com.sukisu.ultra.ui.viewmodel.toJSON +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.ScrollBehavior +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.icons.useful.Back +import top.yukonga.miuix.kmp.icon.icons.useful.Confirm +import top.yukonga.miuix.kmp.icon.icons.useful.Delete +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.utils.getWindowSize +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) -@Destination @Composable +@Destination fun TemplateEditorScreen( navigator: ResultBackNavigator, initialTemplate: TemplateViewModel.TemplateInfo, @@ -56,7 +84,12 @@ fun TemplateEditorScreen( mutableStateOf(initialTemplate) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val scrollBehavior = MiuixScrollBehavior() + val hazeState = remember { HazeState() } + val hazeStyle = HazeStyle( + backgroundColor = colorScheme.background, + tint = HazeTint(colorScheme.background.copy(0.8f)) + ) BackHandler { navigator.navigateBack(result = !readOnly) @@ -64,15 +97,9 @@ fun TemplateEditorScreen( Scaffold( topBar = { - val author = - if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else "" - val readOnlyHint = if (readOnly) { - " - ${stringResource(id = R.string.app_profile_template_readonly)}" - } else { - "" - } - val titleSummary = "${initialTemplate.id}$author$readOnlyHint" val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed) + val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) + val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) val context = LocalContext.current TopBar( @@ -84,7 +111,6 @@ fun TemplateEditorScreen( stringResource(R.string.app_profile_template_edit) }, readOnly = readOnly, - summary = titleSummary, onBack = dropUnlessResumed { navigator.navigateBack(result = !readOnly) }, onDelete = { if (deleteAppProfileTemplate(template.id)) { @@ -92,106 +118,156 @@ fun TemplateEditorScreen( } }, onSave = { + when (idCheck(template.id)) { + 0 -> Unit + + 1 -> { + Toast.makeText(context, idConflictError, Toast.LENGTH_SHORT).show() + return@TopBar + } + + 2 -> { + Toast.makeText(context, idInvalidError, Toast.LENGTH_SHORT).show() + return@TopBar + } + } if (saveTemplate(template, isCreation)) { navigator.navigateBack(result = true) } else { Toast.makeText(context, saveTemplateFailed, Toast.LENGTH_SHORT).show() } }, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + hazeState = hazeState, + hazeStyle = hazeStyle, ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + popupHost = { }, + contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> - Column( + LazyColumn( modifier = Modifier - .padding(innerPadding) + .height(getWindowSize().height.dp) + .scrollEndHaptic() + .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) + .hazeSource(state = hazeState) .pointerInteropFilter { // disable click and ripple if readOnly readOnly - } + }, + contentPadding = innerPadding, + overscrollEffect = null ) { - if (isCreation) { - var errorHint by remember { - mutableStateOf("") - } - val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) - val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) - TextEdit( - label = stringResource(id = R.string.app_profile_template_id), - text = template.id, - errorHint = errorHint, - isError = errorHint.isNotEmpty() - ) { value -> - errorHint = if (isTemplateExist(value)) { - idConflictError - } else if (!isValidTemplateId(value)) { - idInvalidError - } else { - "" + item { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + ) { + var errorHint by remember { + mutableStateOf(false) } - template = template.copy(id = value) - } - } - TextEdit( - label = stringResource(id = R.string.app_profile_template_name), - text = template.name - ) { value -> - template.copy(name = value).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + TextEdit( + label = stringResource(id = R.string.app_profile_template_name), + text = template.name + ) { value -> + template.copy(name = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this } } - template = this - } - } - TextEdit( - label = stringResource(id = R.string.app_profile_template_description), - text = template.description - ) { value -> - template.copy(description = value).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + + TextEdit( + label = stringResource(id = R.string.app_profile_template_id), + text = template.id, + isError = errorHint + ) { value -> + errorHint = if (value.isEmpty()) { + false + } else if (isTemplateExist(value)) { + true + } else if (!isValidTemplateId(value)) { + true + } else { + false + } + template = template.copy(id = value) + } + TextEdit( + label = stringResource(R.string.module_author), + text = template.author + ) { value -> + template.copy(author = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this } } - template = this - } - } - RootProfileConfig(fixedName = true, - profile = toNativeProfile(template), - onProfileChange = { - template.copy( - uid = it.uid, - gid = it.gid, - groups = it.groups, - capabilities = it.capabilities, - context = it.context, - namespace = it.namespace, - rules = it.rules.split("\n") - ).run { - if (autoSave) { - if (!saveTemplate(this)) { - // failed - return@run + TextEdit( + label = stringResource(id = R.string.app_profile_template_description), + text = template.description + ) { value -> + template.copy(description = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this + } + } + + RootProfileConfig( + fixedName = true, + profile = toNativeProfile(template), + onProfileChange = { + template.copy( + uid = it.uid, + gid = it.gid, + groups = it.groups, + capabilities = it.capabilities, + context = it.context, + namespace = it.namespace, + rules = it.rules.split("\n") + ).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this } } - template = this - } - }) + ) + } + Spacer( + Modifier.height( + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + ) + ) + } } } } fun toNativeProfile(templateInfo: TemplateViewModel.TemplateInfo): Natives.Profile { - return Natives.Profile().copy(rootTemplate = templateInfo.id, + return Natives.Profile().copy( + rootTemplate = templateInfo.id, uid = templateInfo.uid, gid = templateInfo.gid, groups = templateInfo.groups, @@ -213,6 +289,10 @@ fun isTemplateValid(template: TemplateViewModel.TemplateInfo): Boolean { return true } +fun idCheck(value: String): Int { + return if (value.isEmpty()) 0 else if (isTemplateExist(value)) 1 else if (!isValidTemplateId(value)) 2 else 0 +} + fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = false): Boolean { if (!isTemplateValid(template)) { return false @@ -227,50 +307,62 @@ fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = return setAppProfileTemplate(template.id, json.toString()) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopBar( title: String, readOnly: Boolean, - summary: String = "", onBack: () -> Unit, onDelete: () -> Unit = {}, onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null + scrollBehavior: ScrollBehavior, + hazeState: HazeState, + hazeStyle: HazeStyle, ) { TopAppBar( - title = { - Column { - Text(title) - if (summary.isNotBlank()) { - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - ) - } - } - }, navigationIcon = { + modifier = Modifier.hazeEffect(hazeState) { + style = hazeStyle + blurRadius = 30.dp + noiseFactor = 0f + }, + color = Color.Transparent, + title = title, + navigationIcon = { IconButton( + modifier = Modifier.padding(start = 16.dp), onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } - }, actions = { - if (readOnly) { - return@TopAppBar - } - IconButton(onClick = onDelete) { + ) { Icon( - Icons.Filled.DeleteForever, - contentDescription = stringResource(id = R.string.app_profile_template_delete) - ) - } - IconButton(onClick = onSave) { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = stringResource(id = R.string.app_profile_template_save) + imageVector = MiuixIcons.Useful.Back, + contentDescription = null, + tint = colorScheme.onBackground + ) + } + }, + actions = { + if (readOnly) { + return@TopAppBar + } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onDelete + ) { + Icon( + imageVector = MiuixIcons.Useful.Delete, + contentDescription = stringResource(id = R.string.app_profile_template_delete), + tint = colorScheme.onBackground + ) + } + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onSave + ) { + Icon( + imageVector = MiuixIcons.Useful.Confirm, + contentDescription = stringResource(id = R.string.app_profile_template_save), + tint = colorScheme.onBackground ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @@ -279,35 +371,22 @@ private fun TopBar( private fun TextEdit( label: String, text: String, - errorHint: String = "", isError: Boolean = false, onValueChange: (String) -> Unit = {} ) { - ListItem(headlineContent = { - val keyboardController = LocalSoftwareKeyboardController.current - OutlinedTextField( - value = text, - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - suffix = { - if (errorHint.isNotBlank()) { - Text( - text = if (isError) errorHint else "", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error - ) - } - }, - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = onValueChange - ) - }) + val editText = remember { mutableStateOf(text) } + EditText( + title = label.uppercase(), + textValue = editText, + onTextValueChange = { newText -> + editText.value = newText + onValueChange(newText) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + ), + isError = isError, + ) } private fun isValidTemplateId(id: String): Boolean { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt deleted file mode 100644 index 0e2206e3..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt +++ /dev/null @@ -1,422 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.Context -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.component.ConfirmResult -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private val SPACING_SMALL = 3.dp -private val SPACING_MEDIUM = 8.dp -private val SPACING_LARGE = 16.dp - -data class UmountPathEntry( - val path: String, - val flags: Int, - val isDefault: Boolean -) - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun UmountManagerScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current - val context = LocalContext.current - val scope = rememberCoroutineScope() - val confirmDialog = rememberConfirmDialog() - - var pathList by remember { mutableStateOf>(emptyList()) } - var isLoading by remember { mutableStateOf(false) } - var showAddDialog by remember { mutableStateOf(false) } - - fun loadPaths() { - scope.launch(Dispatchers.IO) { - isLoading = true - val result = listUmountPaths() - val entries = parseUmountPaths(result) - withContext(Dispatchers.Main) { - pathList = entries - isLoading = false - } - } - } - - LaunchedEffect(Unit) { - loadPaths() - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.umount_path_manager)) }, - navigationIcon = { - IconButton(onClick = { navigator.navigateUp() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) - } - }, - actions = { - IconButton(onClick = { loadPaths() }) { - Icon(Icons.Filled.Refresh, contentDescription = null) - } - }, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy( - alpha = CardConfig.cardAlpha - ) - ) - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { showAddDialog = true } - ) { - Icon(Icons.Filled.Add, contentDescription = null) - } - }, - snackbarHost = { SnackbarHost(snackBarHost) } - ) { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - .nestedScroll(scrollBehavior.nestedScrollConnection) - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(SPACING_LARGE), - colors = getCardColors(MaterialTheme.colorScheme.primaryContainer), - elevation = getCardElevation() - ) { - Column( - modifier = Modifier.padding(SPACING_LARGE) - ) { - Icon( - imageVector = Icons.Filled.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer - ) - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - Text( - text = stringResource(R.string.umount_path_restart_notice), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - - if (isLoading) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), - verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) - ) { - items(pathList, key = { it.path }) { entry -> - UmountPathCard( - entry = entry, - onDelete = { - scope.launch(Dispatchers.IO) { - val success = removeUmountPath(entry.path) - withContext(Dispatchers.Main) { - if (success) { - snackBarHost.showSnackbar( - context.getString(R.string.umount_path_removed) - ) - loadPaths() - } else { - snackBarHost.showSnackbar( - context.getString(R.string.operation_failed) - ) - } - } - } - } - ) - } - - item { - Spacer(modifier = Modifier.height(SPACING_LARGE)) - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = SPACING_LARGE), - horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) - ) { - Button( - onClick = { - scope.launch { - if (confirmDialog.awaitConfirm( - title = context.getString(R.string.confirm_action), - content = context.getString(R.string.confirm_clear_custom_paths) - ) == ConfirmResult.Confirmed) { - withContext(Dispatchers.IO) { - val success = clearCustomUmountPaths() - withContext(Dispatchers.Main) { - if (success) { - snackBarHost.showSnackbar( - context.getString(R.string.custom_paths_cleared) - ) - loadPaths() - } else { - snackBarHost.showSnackbar( - context.getString(R.string.operation_failed) - ) - } - } - } - } - } - }, - modifier = Modifier.weight(1f) - ) { - Icon(Icons.Filled.DeleteForever, contentDescription = null) - Spacer(modifier = Modifier.width(SPACING_MEDIUM)) - Text(stringResource(R.string.clear_custom_paths)) - } - - Button( - onClick = { - scope.launch(Dispatchers.IO) { - val success = applyUmountConfigToKernel() - withContext(Dispatchers.Main) { - if (success) { - snackBarHost.showSnackbar( - context.getString(R.string.config_applied) - ) - } else { - snackBarHost.showSnackbar( - context.getString(R.string.operation_failed) - ) - } - } - } - }, - modifier = Modifier.weight(1f) - ) { - Icon(Icons.Filled.Check, contentDescription = null) - Spacer(modifier = Modifier.width(SPACING_MEDIUM)) - Text(stringResource(R.string.apply_config)) - } - } - } - } - } - } - - if (showAddDialog) { - AddUmountPathDialog( - onDismiss = { showAddDialog = false }, - onConfirm = { path, flags -> - showAddDialog = false - - scope.launch(Dispatchers.IO) { - val success = addUmountPath(path, flags) - withContext(Dispatchers.Main) { - if (success) { - saveUmountConfig() - snackBarHost.showSnackbar( - context.getString(R.string.umount_path_added) - ) - loadPaths() - } else { - snackBarHost.showSnackbar( - context.getString(R.string.operation_failed) - ) - } - } - } - } - ) - } - } -} - -@Composable -fun UmountPathCard( - entry: UmountPathEntry, - onDelete: () -> Unit -) { - val confirmDialog = rememberConfirmDialog() - val scope = rememberCoroutineScope() - val context = LocalContext.current - - Card( - modifier = Modifier.fillMaxWidth(), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), - elevation = getCardElevation() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(SPACING_LARGE), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Filled.Folder, - contentDescription = null, - tint = if (entry.isDefault) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(24.dp) - ) - - Spacer(modifier = Modifier.width(SPACING_LARGE)) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = entry.path, - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(SPACING_SMALL)) - Text( - text = buildString { - append(context.getString(R.string.flags)) - append(": ") - append(entry.flags.toUmountFlagName(context)) - if (entry.isDefault) { - append(" | ") - append(context.getString(R.string.default_entry)) - } - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (!entry.isDefault) { - IconButton( - onClick = { - scope.launch { - if (confirmDialog.awaitConfirm( - title = context.getString(R.string.confirm_delete), - content = context.getString(R.string.confirm_delete_umount_path, entry.path) - ) == ConfirmResult.Confirmed) { - onDelete() - } - } - } - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - } - } - } -} - -@Composable -fun AddUmountPathDialog( - onDismiss: () -> Unit, - onConfirm: (String, Int) -> Unit -) { - var path by rememberSaveable { mutableStateOf("") } - var flags by rememberSaveable { mutableStateOf("-1") } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.add_umount_path)) }, - text = { - Column { - OutlinedTextField( - value = path, - onValueChange = { path = it }, - label = { Text(stringResource(R.string.mount_path)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - Spacer(modifier = Modifier.height(SPACING_MEDIUM)) - - OutlinedTextField( - value = flags, - onValueChange = { flags = it }, - label = { Text(stringResource(R.string.umount_flags)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - supportingText = { Text(stringResource(R.string.umount_flags_hint)) } - ) - } - }, - confirmButton = { - TextButton( - onClick = { - val flagsInt = flags.toIntOrNull() ?: -1 - onConfirm(path, flagsInt) - }, - enabled = path.isNotBlank() - ) { - Text(stringResource(android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(android.R.string.cancel)) - } - } - ) -} - -private fun parseUmountPaths(output: String): List { - val lines = output.lines().filter { it.isNotBlank() } - if (lines.size < 2) return emptyList() - - return lines.drop(2).mapNotNull { line -> - val parts = line.trim().split(Regex("\\s+")) - if (parts.size >= 3) { - UmountPathEntry( - path = parts[0], - flags = parts[1].toIntOrNull() ?: -1, - isDefault = parts[2].equals("Yes", ignoreCase = true) - ) - } else null - } -} - -private fun Int.toUmountFlagName(context: Context): String { - return when (this) { - -1 -> context.getString(R.string.mnt_detach) - else -> this.toString() - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt deleted file mode 100644 index 00353b2b..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt +++ /dev/null @@ -1,2211 +0,0 @@ -package com.sukisu.ultra.ui.susfs - -import android.annotation.SuppressLint -import android.content.Context -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.susfs.component.* -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.susfs.util.SuSFSManager -import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 -import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159 -import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion1512 -import com.sukisu.ultra.ui.util.getSuSFSVersion -import com.sukisu.ultra.ui.util.isAbDevice -import kotlinx.coroutines.launch -import java.io.File -import java.text.SimpleDateFormat -import java.util.* - -/** - * 标签页枚举类 - */ -enum class SuSFSTab(val displayNameRes: Int) { - BASIC_SETTINGS(R.string.susfs_tab_basic_settings), - SUS_PATHS(R.string.susfs_tab_sus_paths), - SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), - SUS_MAPS(R.string.susfs_tab_sus_maps), - SUS_MOUNTS(R.string.susfs_tab_sus_mounts), - TRY_UMOUNT(R.string.susfs_tab_try_umount), - KSTAT_CONFIG(R.string.susfs_tab_kstat_config), - PATH_SETTINGS(R.string.susfs_tab_path_settings), - ENABLED_FEATURES(R.string.susfs_tab_enabled_features); - - companion object { - fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean, isSusVersion1512: Boolean): List { - return when { - isSusVersion1512 -> entries.toList() - isSusVersion159 -> entries.filter { it != SUS_MAPS} - isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS && it != SUS_MAPS } - else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS && it != SUS_MAPS } - } - } - } -} - -/** - * SuSFS配置界面 - */ -@SuppressLint("SdCardPath", "AutoboxingStateCreation") -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun SuSFSConfigScreen( - navigator: DestinationsNavigator -) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - - var selectedTab by remember { mutableStateOf(SuSFSTab.BASIC_SETTINGS) } - var unameValue by remember { mutableStateOf("") } - var buildTimeValue by remember { mutableStateOf("") } - var isLoading by remember { mutableStateOf(false) } - var showConfirmReset by remember { mutableStateOf(false) } - var autoStartEnabled by remember { mutableStateOf(false) } - var executeInPostFsData by remember { mutableStateOf(false) } - var enableHideBl by remember { mutableStateOf(true) } - var enableCleanupResidue by remember { mutableStateOf(false) } - var enableAvcLogSpoofing by remember { mutableStateOf(false) } - - // 槽位信息相关状态 - var slotInfoList by remember { mutableStateOf(emptyList()) } - var currentActiveSlot by remember { mutableStateOf("") } - var isLoadingSlotInfo by remember { mutableStateOf(false) } - var showSlotInfoDialog by remember { mutableStateOf(false) } - - // 路径管理相关状态 - var susPaths by remember { mutableStateOf(emptySet()) } - var susLoopPaths by remember { mutableStateOf(emptySet()) } - var susMaps by remember { mutableStateOf(emptySet()) } - var susMounts by remember { mutableStateOf(emptySet()) } - var tryUmounts by remember { mutableStateOf(emptySet()) } - var androidDataPath by remember { mutableStateOf("") } - var sdcardPath by remember { mutableStateOf("") } - - // SUS挂载隐藏控制状态 - var hideSusMountsForAllProcs by remember { mutableStateOf(true) } - - var umountForZygoteIsoService by remember { mutableStateOf(false) } - - // Kstat配置相关状态 - var kstatConfigs by remember { mutableStateOf(emptySet()) } - var addKstatPaths by remember { mutableStateOf(emptySet()) } - - // 启用功能状态相关 - var enabledFeatures by remember { mutableStateOf(emptyList()) } - var isLoadingFeatures by remember { mutableStateOf(false) } - - // 应用列表相关状态 - var installedApps by remember { mutableStateOf(emptyList()) } - - // 对话框状态 - var showAddPathDialog by remember { mutableStateOf(false) } - var showAddLoopPathDialog by remember { mutableStateOf(false) } - var showAddSusMapDialog by remember { mutableStateOf(false) } - var showAddAppPathDialog by remember { mutableStateOf(false) } - var showAddMountDialog by remember { mutableStateOf(false) } - var showAddUmountDialog by remember { mutableStateOf(false) } - var showAddKstatStaticallyDialog by remember { mutableStateOf(false) } - var showAddKstatDialog by remember { mutableStateOf(false) } - - // 编辑状态 - var editingPath by remember { mutableStateOf(null) } - var editingLoopPath by remember { mutableStateOf(null) } - var editingSusMap by remember { mutableStateOf(null) } - var editingMount by remember { mutableStateOf(null) } - var editingUmount by remember { mutableStateOf(null) } - var editingKstatConfig by remember { mutableStateOf(null) } - var editingKstatPath by remember { mutableStateOf(null) } - - // 重置确认对话框状态 - var showResetPathsDialog by remember { mutableStateOf(false) } - var showResetLoopPathsDialog by remember { mutableStateOf(false) } - var showResetSusMapsDialog by remember { mutableStateOf(false) } - var showResetMountsDialog by remember { mutableStateOf(false) } - var showResetUmountsDialog by remember { mutableStateOf(false) } - var showResetKstatDialog by remember { mutableStateOf(false) } - - // 备份还原相关状态 - var showBackupDialog by remember { mutableStateOf(false) } - var showRestoreDialog by remember { mutableStateOf(false) } - var showRestoreConfirmDialog by remember { mutableStateOf(false) } - var selectedBackupFile by remember { mutableStateOf(null) } - var backupInfo by remember { mutableStateOf(null) } - - var isNavigating by remember { mutableStateOf(false) } - - val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159(), isSusVersion1512()) - - // 实时判断是否可以启用开机自启动 - val canEnableAutoStart by remember { - derivedStateOf { - SuSFSManager.hasConfigurationForAutoStart(context) - } - } - - var showVersionMismatchDialog by remember { mutableStateOf(false) } - - if (showVersionMismatchDialog) { - AlertDialog( - onDismissRequest = { showVersionMismatchDialog = false }, - title = { - Text( - text = stringResource(R.string.warning), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - }, - text = { - Text( - stringResource( - R.string.susfs_version_mismatch, - try { getSuSFSVersion() } catch (_: Exception) { "unknown" }, - SuSFSManager.MAX_SUSFS_VERSION - ) - ) - }, - confirmButton = { - TextButton( - onClick = { showVersionMismatchDialog = false }, - modifier = Modifier.padding(8.dp) - ) { - Text(stringResource(R.string.confirm)) - } - } - ) - } - - // 文件选择器 - val backupFileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("application/json") - ) { uri -> - uri?.let { fileUri -> - val fileName = SuSFSManager.getDefaultBackupFileName() - val tempFile = File(context.cacheDir, fileName) - coroutineScope.launch { - isLoading = true - val success = SuSFSManager.createBackup(context, tempFile.absolutePath) - if (success) { - try { - context.contentResolver.openOutputStream(fileUri)?.use { outputStream -> - tempFile.inputStream().use { inputStream -> - inputStream.copyTo(outputStream) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - tempFile.delete() - } - isLoading = false - showBackupDialog = false - } - } - } - - val restoreFileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument() - ) { uri -> - uri?.let { fileUri -> - coroutineScope.launch { - try { - val tempFile = File(context.cacheDir, "temp_restore.susfs_backup") - context.contentResolver.openInputStream(fileUri)?.use { inputStream -> - tempFile.outputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - // 验证备份文件 - val backup = SuSFSManager.validateBackupFile(tempFile.absolutePath) - if (backup != null) { - selectedBackupFile = tempFile.absolutePath - backupInfo = backup - showRestoreConfirmDialog = true - } - tempFile.deleteOnExit() - } catch (e: Exception) { - e.printStackTrace() - } - showRestoreDialog = false - } - } - } - - // 加载启用功能状态 - fun loadEnabledFeatures() { - coroutineScope.launch { - isLoadingFeatures = true - enabledFeatures = SuSFSManager.getEnabledFeatures(context) - isLoadingFeatures = false - } - } - - // 加载应用列表 - fun loadInstalledApps() { - coroutineScope.launch { - installedApps = SuSFSManager.getInstalledApps() - } - } - - // 加载槽位信息 - fun loadSlotInfo() { - coroutineScope.launch { - isLoadingSlotInfo = true - slotInfoList = SuSFSManager.getCurrentSlotInfo() - currentActiveSlot = SuSFSManager.getCurrentActiveSlot() - isLoadingSlotInfo = false - } - } - - // 加载当前配置 - LaunchedEffect(Unit) { - coroutineScope.launch { - try { - val version = getSuSFSVersion() - val binaryName = "ksu_susfs_${version.removePrefix("v")}" - - val isBinaryAvailable = try { - context.assets.open(binaryName).use { true } - } catch (_: Exception) { false } - - if (!isBinaryAvailable) { - showVersionMismatchDialog = true - } - } catch (_: Exception) { - } - - unameValue = SuSFSManager.getUnameValue(context) - buildTimeValue = SuSFSManager.getBuildTimeValue(context) - autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) - executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) - susPaths = SuSFSManager.getSusPaths(context) - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - susMaps = SuSFSManager.getSusMaps(context) - susMounts = SuSFSManager.getSusMounts(context) - tryUmounts = SuSFSManager.getTryUmounts(context) - androidDataPath = SuSFSManager.getAndroidDataPath(context) - sdcardPath = SuSFSManager.getSdcardPath(context) - kstatConfigs = SuSFSManager.getKstatConfigs(context) - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) - enableHideBl = SuSFSManager.getEnableHideBl(context) - enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) - umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) - enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) - - loadSlotInfo() - } - } - - // 当切换到启用功能状态标签页时加载数据 - LaunchedEffect(selectedTab) { - if (selectedTab == SuSFSTab.ENABLED_FEATURES) { - loadEnabledFeatures() - } - } - - // 当配置变化时,自动调整开机自启动状态 - LaunchedEffect(canEnableAutoStart) { - if (!canEnableAutoStart && autoStartEnabled) { - autoStartEnabled = false - SuSFSManager.configureAutoStart(context, false) - } - } - - // 备份对话框 - if (showBackupDialog) { - AlertDialog( - onDismissRequest = { showBackupDialog = false }, - title = { - Text( - text = stringResource(R.string.susfs_backup_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Text(stringResource(R.string.susfs_backup_description)) - }, - confirmButton = { - Button( - onClick = { - val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) - val timestamp = dateFormat.format(Date()) - backupFileLauncher.launch("SuSFS_Config_$timestamp.susfs_backup") - }, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_backup_create)) - } - }, - dismissButton = { - TextButton( - onClick = { showBackupDialog = false }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } - - // 还原对话框 - if (showRestoreDialog) { - AlertDialog( - onDismissRequest = { showRestoreDialog = false }, - title = { - Text( - text = stringResource(R.string.susfs_restore_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Text(stringResource(R.string.susfs_restore_description)) - }, - confirmButton = { - Button( - onClick = { - restoreFileLauncher.launch(arrayOf("application/json", "*/*")) - }, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_restore_select_file)) - } - }, - dismissButton = { - TextButton( - onClick = { showRestoreDialog = false }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } - - // 还原确认对话框 - if (showRestoreConfirmDialog && backupInfo != null) { - AlertDialog( - onDismissRequest = { - showRestoreConfirmDialog = false - selectedBackupFile = null - backupInfo = null - }, - title = { - Text( - text = stringResource(R.string.susfs_restore_confirm_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(stringResource(R.string.susfs_restore_confirm_description)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - Text( - text = stringResource(R.string.susfs_backup_info_date, - dateFormat.format(Date(backupInfo!!.timestamp))), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = stringResource(R.string.susfs_backup_info_device, backupInfo!!.deviceInfo), - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = stringResource(R.string.susfs_backup_info_version, backupInfo!!.version), - style = MaterialTheme.typography.bodyMedium - ) - } - } - } - }, - confirmButton = { - Button( - onClick = { - selectedBackupFile?.let { filePath -> - coroutineScope.launch { - isLoading = true - val success = SuSFSManager.restoreFromBackup(context, filePath) - if (success) { - // 重新加载所有配置 - unameValue = SuSFSManager.getUnameValue(context) - buildTimeValue = SuSFSManager.getBuildTimeValue(context) - autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) - executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) - susPaths = SuSFSManager.getSusPaths(context) - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - susMaps = SuSFSManager.getSusMaps(context) - susMounts = SuSFSManager.getSusMounts(context) - tryUmounts = SuSFSManager.getTryUmounts(context) - androidDataPath = SuSFSManager.getAndroidDataPath(context) - sdcardPath = SuSFSManager.getSdcardPath(context) - kstatConfigs = SuSFSManager.getKstatConfigs(context) - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) - enableHideBl = SuSFSManager.getEnableHideBl(context) - enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) - umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) - enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) - } - isLoading = false - showRestoreConfirmDialog = false - selectedBackupFile = null - backupInfo = null - } - } - }, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_restore_confirm)) - } - }, - dismissButton = { - TextButton( - onClick = { - showRestoreConfirmDialog = false - selectedBackupFile = null - backupInfo = null - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } - - // 槽位信息对话框 - SlotInfoDialog( - showDialog = showSlotInfoDialog, - onDismiss = { showSlotInfoDialog = false }, - slotInfoList = slotInfoList, - currentActiveSlot = currentActiveSlot, - isLoadingSlotInfo = isLoadingSlotInfo, - onRefresh = { loadSlotInfo() }, - onUseUname = { uname -> - unameValue = uname - showSlotInfoDialog = false - }, - onUseBuildTime = { buildTime -> - buildTimeValue = buildTime - showSlotInfoDialog = false - } - ) - - // 各种对话框 - AddPathDialog( - showDialog = showAddPathDialog, - onDismiss = { - showAddPathDialog = false - editingPath = null - }, - onConfirm = { path -> - coroutineScope.launch { - isLoading = true - val success = if (editingPath != null) { - SuSFSManager.editSusPath(context, editingPath!!, path) - } else { - SuSFSManager.addSusPath(context, path) - } - if (success) { - susPaths = SuSFSManager.getSusPaths(context) - } - isLoading = false - showAddPathDialog = false - editingPath = null - } - }, - isLoading = isLoading, - titleRes = if (editingPath != null) R.string.susfs_edit_sus_path else R.string.susfs_add_sus_path, - labelRes = R.string.susfs_path_label, - placeholderRes = R.string.susfs_path_placeholder, - initialValue = editingPath ?: "" - ) - - AddPathDialog( - showDialog = showAddLoopPathDialog, - onDismiss = { - showAddLoopPathDialog = false - editingLoopPath = null - }, - onConfirm = { path -> - coroutineScope.launch { - isLoading = true - val success = if (editingLoopPath != null) { - SuSFSManager.editSusLoopPath(context, editingLoopPath!!, path) - } else { - SuSFSManager.addSusLoopPath(context, path) - } - if (success) { - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - } - isLoading = false - showAddLoopPathDialog = false - editingLoopPath = null - } - }, - isLoading = isLoading, - titleRes = if (editingLoopPath != null) R.string.susfs_edit_sus_loop_path else R.string.susfs_add_sus_loop_path, - labelRes = R.string.susfs_loop_path_label, - placeholderRes = R.string.susfs_loop_path_placeholder, - initialValue = editingLoopPath ?: "" - ) - - AddPathDialog( - showDialog = showAddSusMapDialog, - onDismiss = { - showAddSusMapDialog = false - editingSusMap = null - }, - onConfirm = { path -> - coroutineScope.launch { - isLoading = true - val success = if (editingSusMap != null) { - SuSFSManager.editSusMap(context, editingSusMap!!, path) - } else { - SuSFSManager.addSusMap(context, path) - } - if (success) { - susMaps = SuSFSManager.getSusMaps(context) - } - isLoading = false - showAddSusMapDialog = false - editingSusMap = null - } - }, - isLoading = isLoading, - titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map, - labelRes = R.string.susfs_sus_map_label, - placeholderRes = R.string.susfs_sus_map_placeholder, - initialValue = editingSusMap ?: "" - ) - - AddAppPathDialog( - showDialog = showAddAppPathDialog, - onDismiss = { showAddAppPathDialog = false }, - onConfirm = { packageNames -> - coroutineScope.launch { - isLoading = true - var successCount = 0 - packageNames.forEach { packageName -> - if (SuSFSManager.addAppPaths(context, packageName)) { - successCount++ - } - } - if (successCount > 0) { - susPaths = SuSFSManager.getSusPaths(context) - } - isLoading = false - showAddAppPathDialog = false - } - }, - isLoading = isLoading, - apps = installedApps, - onLoadApps = { loadInstalledApps() }, - existingSusPaths = susPaths - ) - - AddPathDialog( - showDialog = showAddMountDialog, - onDismiss = { - showAddMountDialog = false - editingMount = null - }, - onConfirm = { mount -> - coroutineScope.launch { - isLoading = true - val success = if (editingMount != null) { - SuSFSManager.editSusMount(context, editingMount!!, mount) - } else { - SuSFSManager.addSusMount(context, mount) - } - if (success) { - susMounts = SuSFSManager.getSusMounts(context) - } - isLoading = false - showAddMountDialog = false - editingMount = null - } - }, - isLoading = isLoading, - titleRes = if (editingMount != null) R.string.susfs_edit_sus_mount else R.string.susfs_add_sus_mount, - labelRes = R.string.susfs_mount_path_label, - placeholderRes = R.string.susfs_path_placeholder, - initialValue = editingMount ?: "" - ) - - AddTryUmountDialog( - showDialog = showAddUmountDialog, - onDismiss = { - showAddUmountDialog = false - editingUmount = null - }, - onConfirm = { path, mode -> - coroutineScope.launch { - isLoading = true - val success = if (editingUmount != null) { - SuSFSManager.editTryUmount(context, editingUmount!!, path, mode) - } else { - SuSFSManager.addTryUmount(context, path, mode) - } - if (success) { - tryUmounts = SuSFSManager.getTryUmounts(context) - } - isLoading = false - showAddUmountDialog = false - editingUmount = null - } - }, - isLoading = isLoading, - initialPath = editingUmount?.split("|")?.get(0) ?: "", - initialMode = editingUmount?.split("|")?.get(1)?.toIntOrNull() ?: 0 - ) - - AddKstatStaticallyDialog( - showDialog = showAddKstatStaticallyDialog, - onDismiss = { - showAddKstatStaticallyDialog = false - editingKstatConfig = null - }, - onConfirm = { path, ino, dev, nlink, size, atime, atimeNsec, mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize -> - coroutineScope.launch { - isLoading = true - val success = if (editingKstatConfig != null) { - SuSFSManager.editKstatConfig( - context, - editingKstatConfig!!, - path, - ino, - dev, - nlink, - size, - atime, - atimeNsec, - mtime, - mtimeNsec, - ctime, - ctimeNsec, - blocks, - blksize - ) - } else { - SuSFSManager.addKstatStatically( - context, path, ino, dev, nlink, size, atime, atimeNsec, - mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize - ) - } - if (success) { - kstatConfigs = SuSFSManager.getKstatConfigs(context) - } - isLoading = false - showAddKstatStaticallyDialog = false - editingKstatConfig = null - } - }, - isLoading = isLoading, - initialConfig = editingKstatConfig ?: "" - ) - - AddPathDialog( - showDialog = showAddKstatDialog, - onDismiss = { - showAddKstatDialog = false - editingKstatPath = null - }, - onConfirm = { path -> - coroutineScope.launch { - isLoading = true - val success = if (editingKstatPath != null) { - SuSFSManager.editAddKstat(context, editingKstatPath!!, path) - } else { - SuSFSManager.addKstat(context, path) - } - if (success) { - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - } - isLoading = false - showAddKstatDialog = false - editingKstatPath = null - } - }, - isLoading = isLoading, - titleRes = if (editingKstatPath != null) R.string.edit_kstat_path_title else R.string.add_kstat_path_title, - labelRes = R.string.file_or_directory_path_label, - placeholderRes = R.string.susfs_path_placeholder, - initialValue = editingKstatPath ?: "" - ) - - // 确认对话框 - ConfirmDialog( - showDialog = showConfirmReset, - onDismiss = { showConfirmReset = false }, - onConfirm = { - showConfirmReset = false - coroutineScope.launch { - isLoading = true - if (SuSFSManager.resetToDefault(context)) { - unameValue = "default" - buildTimeValue = "default" - autoStartEnabled = false - } - isLoading = false - } - }, - titleRes = R.string.susfs_reset_confirm_title, - messageRes = R.string.susfs_reset_confirm_title, - isLoading = isLoading, - isDestructive = true - ) - - // 重置对话框 - ConfirmDialog( - showDialog = showResetPathsDialog, - onDismiss = { showResetPathsDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveSusPaths(context, emptySet()) - susPaths = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetPathsDialog = false - } - }, - titleRes = R.string.susfs_reset_paths_title, - messageRes = R.string.susfs_reset_paths_message, - isLoading = isLoading, - isDestructive = true - ) - - ConfirmDialog( - showDialog = showResetLoopPathsDialog, - onDismiss = { showResetLoopPathsDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveSusLoopPaths(context, emptySet()) - susLoopPaths = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetLoopPathsDialog = false - } - }, - titleRes = R.string.susfs_reset_loop_paths_title, - messageRes = R.string.susfs_reset_loop_paths_message, - isLoading = isLoading, - isDestructive = true - ) - - ConfirmDialog( - showDialog = showResetSusMapsDialog, - onDismiss = { showResetSusMapsDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveSusMaps(context, emptySet()) - susMaps = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetSusMapsDialog = false - } - }, - titleRes = R.string.susfs_reset_sus_maps_title, - messageRes = R.string.susfs_reset_sus_maps_message, - isLoading = isLoading, - isDestructive = true - ) - - ConfirmDialog( - showDialog = showResetMountsDialog, - onDismiss = { showResetMountsDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveSusMounts(context, emptySet()) - susMounts = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetMountsDialog = false - } - }, - titleRes = R.string.susfs_reset_mounts_title, - messageRes = R.string.susfs_reset_mounts_message, - isLoading = isLoading, - isDestructive = true - ) - - ConfirmDialog( - showDialog = showResetUmountsDialog, - onDismiss = { showResetUmountsDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveTryUmounts(context, emptySet()) - tryUmounts = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetUmountsDialog = false - } - }, - titleRes = R.string.susfs_reset_umounts_title, - messageRes = R.string.susfs_reset_umounts_message, - isLoading = isLoading, - isDestructive = true - ) - - ConfirmDialog( - showDialog = showResetKstatDialog, - onDismiss = { showResetKstatDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.saveKstatConfigs(context, emptySet()) - SuSFSManager.saveAddKstatPaths(context, emptySet()) - kstatConfigs = emptySet() - addKstatPaths = emptySet() - if (SuSFSManager.isAutoStartEnabled(context)) { - SuSFSManager.configureAutoStart(context, true) - } - isLoading = false - showResetKstatDialog = false - } - }, - titleRes = R.string.reset_kstat_config_title, - messageRes = R.string.reset_kstat_config_message, - isLoading = isLoading, - isDestructive = true - ) - - // 主界面布局 - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_config_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - } - }, - navigationIcon = { - IconButton(onClick = { - if (!isNavigating) { - isNavigating = true - navigator.popBackStack() - } - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) - }, - bottomBar = { - // 统一的底部按钮栏 - Surface( - modifier = Modifier.fillMaxWidth(), - color = Color.Transparent, - shadowElevation = 0.dp - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - when (selectedTab) { - SuSFSTab.BASIC_SETTINGS -> { - // 应用按钮 - Button( - onClick = { - if (unameValue.isNotBlank() || buildTimeValue.isNotBlank()) { - coroutineScope.launch { - isLoading = true - val finalUnameValue = unameValue.trim().ifBlank { "default" } - val finalBuildTimeValue = buildTimeValue.trim().ifBlank { "default" } - val success = SuSFSManager.setUname(context, finalUnameValue, finalBuildTimeValue) - if (success) { - SuSFSManager.saveExecuteInPostFsData(context, executeInPostFsData) - SuSFSManager.saveEnableHideBl(context, enableHideBl) - SuSFSManager.saveEnableCleanupResidue(context, enableCleanupResidue) - SuSFSManager.saveEnableAvcLogSpoofing(context, enableAvcLogSpoofing) - } - isLoading = false - } - } - }, - enabled = !isLoading && (unameValue.isNotBlank() || buildTimeValue.isNotBlank()), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .weight(1f) - .height(40.dp) - ) { - Text( - stringResource(R.string.susfs_apply), - fontWeight = FontWeight.Medium - ) - } - - // 重置按钮 - OutlinedButton( - onClick = { showConfirmReset = true }, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .weight(1f) - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_to_default), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.SUS_PATHS -> { - OutlinedButton( - onClick = { showResetPathsDialog = true }, - enabled = !isLoading && susPaths.isNotEmpty(), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_paths_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.SUS_LOOP_PATHS -> { - OutlinedButton( - onClick = { showResetLoopPathsDialog = true }, - enabled = !isLoading && susLoopPaths.isNotEmpty(), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_loop_paths_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.SUS_MAPS -> { - OutlinedButton( - onClick = { showResetSusMapsDialog = true }, - enabled = !isLoading && susMaps.isNotEmpty(), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_sus_maps_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.SUS_MOUNTS -> { - OutlinedButton( - onClick = { showResetMountsDialog = true }, - enabled = !isLoading && susMounts.isNotEmpty(), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_mounts_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.TRY_UMOUNT -> { - OutlinedButton( - onClick = { showResetUmountsDialog = true }, - enabled = !isLoading && tryUmounts.isNotEmpty(), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_umounts_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.KSTAT_CONFIG -> { - OutlinedButton( - onClick = { showResetKstatDialog = true }, - enabled = !isLoading && (kstatConfigs.isNotEmpty() || addKstatPaths.isNotEmpty()), - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.reset_kstat_config_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.PATH_SETTINGS -> { - OutlinedButton( - onClick = { - androidDataPath = "/sdcard/Android/data" - sdcardPath = "/sdcard" - coroutineScope.launch { - isLoading = true - SuSFSManager.setAndroidDataPath(context, androidDataPath) - SuSFSManager.setSdcardPath(context, sdcardPath) - isLoading = false - } - }, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.RestoreFromTrash, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_reset_path_title), - fontWeight = FontWeight.Medium - ) - } - } - - SuSFSTab.ENABLED_FEATURES -> { - Button( - onClick = { loadEnabledFeatures() }, - enabled = !isLoadingFeatures, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.refresh), - fontWeight = FontWeight.Medium - ) - } - } - } - } - } - }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 12.dp) - ) { - // 标签页 - PrimaryScrollableTabRow( - selectedTabIndex = allTabs.indexOf(selectedTab), - modifier = Modifier.fillMaxWidth(), - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - edgePadding = 0.dp - ) { - allTabs.forEach { tab -> - Tab( - selected = selectedTab == tab, - onClick = { selectedTab = tab }, - text = { - Text( - text = stringResource(tab.displayNameRes), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontSize = 13.sp, - fontWeight = if (selectedTab == tab) FontWeight.Bold else FontWeight.Normal - ) - }, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // 标签页内容 - Box( - modifier = Modifier.fillMaxSize() - ) { - when (selectedTab) { - SuSFSTab.BASIC_SETTINGS -> { - BasicSettingsContent( - unameValue = unameValue, - onUnameValueChange = { unameValue = it }, - buildTimeValue = buildTimeValue, - onBuildTimeValueChange = { buildTimeValue = it }, - executeInPostFsData = executeInPostFsData, - onExecuteInPostFsDataChange = { executeInPostFsData = it }, - autoStartEnabled = autoStartEnabled, - canEnableAutoStart = canEnableAutoStart, - isLoading = isLoading, - onAutoStartToggle = { enabled -> - if (canEnableAutoStart) { - coroutineScope.launch { - isLoading = true - if (SuSFSManager.configureAutoStart(context, enabled)) { - autoStartEnabled = enabled - } - isLoading = false - } - } - }, - onShowSlotInfo = { showSlotInfoDialog = true }, - context = context, - onShowBackupDialog = { showBackupDialog = true }, - onShowRestoreDialog = { showRestoreDialog = true }, - enableHideBl = enableHideBl, - onEnableHideBlChange = { enabled -> - enableHideBl = enabled - SuSFSManager.saveEnableHideBl(context, enabled) - if (SuSFSManager.isAutoStartEnabled(context)) { - coroutineScope.launch { - SuSFSManager.configureAutoStart(context, true) - } - } - }, - enableCleanupResidue = enableCleanupResidue, - onEnableCleanupResidueChange = { enabled -> - enableCleanupResidue = enabled - SuSFSManager.saveEnableCleanupResidue(context, enabled) - if (SuSFSManager.isAutoStartEnabled(context)) { - coroutineScope.launch { - SuSFSManager.configureAutoStart(context, true) - } - } - }, - enableAvcLogSpoofing = enableAvcLogSpoofing, - onEnableAvcLogSpoofingChange = { enabled -> - coroutineScope.launch { - isLoading = true - val success = SuSFSManager.setEnableAvcLogSpoofing(context, enabled) - if (success) { - enableAvcLogSpoofing = enabled - } - isLoading = false - } - } - ) - } - SuSFSTab.SUS_PATHS -> { - SusPathsContent( - susPaths = susPaths, - isLoading = isLoading, - onAddPath = { showAddPathDialog = true }, - onAddAppPath = { showAddAppPathDialog = true }, - onRemovePath = { path -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeSusPath(context, path)) { - susPaths = SuSFSManager.getSusPaths(context) - } - isLoading = false - } - }, - onEditPath = { path -> - editingPath = path - showAddPathDialog = true - }, - forceRefreshApps = selectedTab == SuSFSTab.SUS_PATHS - ) - } - SuSFSTab.SUS_LOOP_PATHS -> { - SusLoopPathsContent( - susLoopPaths = susLoopPaths, - isLoading = isLoading, - onAddLoopPath = { showAddLoopPathDialog = true }, - onRemoveLoopPath = { path -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeSusLoopPath(context, path)) { - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - } - isLoading = false - } - }, - onEditLoopPath = { path -> - editingLoopPath = path - showAddLoopPathDialog = true - } - ) - } - SuSFSTab.SUS_MAPS -> { - SusMapsContent( - susMaps = susMaps, - isLoading = isLoading, - onAddSusMap = { showAddSusMapDialog = true }, - onRemoveSusMap = { map -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeSusMap(context, map)) { - susMaps = SuSFSManager.getSusMaps(context) - } - isLoading = false - } - }, - onEditSusMap = { map -> - editingSusMap = map - showAddSusMapDialog = true - } - ) - } - SuSFSTab.SUS_MOUNTS -> { - val isSusVersion158 = remember { isSusVersion158() } - - SusMountsContent( - susMounts = susMounts, - hideSusMountsForAllProcs = hideSusMountsForAllProcs, - isSusVersion158 = isSusVersion158, - isLoading = isLoading, - onAddMount = { showAddMountDialog = true }, - onRemoveMount = { mount -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeSusMount(context, mount)) { - susMounts = SuSFSManager.getSusMounts(context) - } - isLoading = false - } - }, - onEditMount = { mount -> - editingMount = mount - showAddMountDialog = true - }, - onToggleHideSusMountsForAllProcs = { hideForAll -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.setHideSusMountsForAllProcs( - context, - hideForAll - ) - ) { - hideSusMountsForAllProcs = hideForAll - } - isLoading = false - } - } - ) - } - - SuSFSTab.TRY_UMOUNT -> { - TryUmountContent( - tryUmounts = tryUmounts, - umountForZygoteIsoService = umountForZygoteIsoService, - isLoading = isLoading, - onAddUmount = { showAddUmountDialog = true }, - onRemoveUmount = { umountEntry -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeTryUmount(context, umountEntry)) { - tryUmounts = SuSFSManager.getTryUmounts(context) - } - isLoading = false - } - }, - onEditUmount = { umountEntry -> - editingUmount = umountEntry - showAddUmountDialog = true - }, - onToggleUmountForZygoteIsoService = { enabled -> - coroutineScope.launch { - isLoading = true - val success = - SuSFSManager.setUmountForZygoteIsoService(context, enabled) - if (success) { - umountForZygoteIsoService = enabled - } - isLoading = false - } - } - ) - } - - SuSFSTab.KSTAT_CONFIG -> { - KstatConfigContent( - kstatConfigs = kstatConfigs, - addKstatPaths = addKstatPaths, - isLoading = isLoading, - onAddKstatStatically = { showAddKstatStaticallyDialog = true }, - onAddKstat = { showAddKstatDialog = true }, - onRemoveKstatConfig = { config -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeKstatConfig(context, config)) { - kstatConfigs = SuSFSManager.getKstatConfigs(context) - } - isLoading = false - } - }, - onEditKstatConfig = { config -> - editingKstatConfig = config - showAddKstatStaticallyDialog = true - }, - onRemoveAddKstat = { path -> - coroutineScope.launch { - isLoading = true - if (SuSFSManager.removeAddKstat(context, path)) { - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - } - isLoading = false - } - }, - onEditAddKstat = { path -> - editingKstatPath = path - showAddKstatDialog = true - }, - onUpdateKstat = { path -> - coroutineScope.launch { - isLoading = true - SuSFSManager.updateKstat(context, path) - isLoading = false - } - }, - onUpdateKstatFullClone = { path -> - coroutineScope.launch { - isLoading = true - SuSFSManager.updateKstatFullClone(context, path) - isLoading = false - } - } - ) - } - SuSFSTab.PATH_SETTINGS -> { - PathSettingsContent( - androidDataPath = androidDataPath, - onAndroidDataPathChange = { androidDataPath = it }, - sdcardPath = sdcardPath, - onSdcardPathChange = { sdcardPath = it }, - isLoading = isLoading, - onSetAndroidDataPath = { - coroutineScope.launch { - isLoading = true - SuSFSManager.setAndroidDataPath(context, androidDataPath.trim()) - isLoading = false - } - }, - onSetSdcardPath = { - coroutineScope.launch { - isLoading = true - SuSFSManager.setSdcardPath(context, sdcardPath.trim()) - isLoading = false - } - } - ) - } - SuSFSTab.ENABLED_FEATURES -> { - EnabledFeaturesContent( - enabledFeatures = enabledFeatures, - onRefresh = { loadEnabledFeatures() } - ) - } - } - } - } - } -} - -/** - * 基本设置内容组件 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun BasicSettingsContent( - unameValue: String, - onUnameValueChange: (String) -> Unit, - buildTimeValue: String, - onBuildTimeValueChange: (String) -> Unit, - executeInPostFsData: Boolean, - onExecuteInPostFsDataChange: (Boolean) -> Unit, - autoStartEnabled: Boolean, - canEnableAutoStart: Boolean, - isLoading: Boolean, - onAutoStartToggle: (Boolean) -> Unit, - onShowSlotInfo: () -> Unit, - context: Context, - onShowBackupDialog: () -> Unit, - onShowRestoreDialog: () -> Unit, - enableHideBl: Boolean, - onEnableHideBlChange: (Boolean) -> Unit, - enableCleanupResidue: Boolean, - onEnableCleanupResidueChange: (Boolean) -> Unit, - enableAvcLogSpoofing: Boolean, - onEnableAvcLogSpoofingChange: (Boolean) -> Unit -) { - var scriptLocationExpanded by remember { mutableStateOf(false) } - val isAbDevice = produceState(initialValue = false) { - value = isAbDevice() - }.value - val isSusVersion159 = isSusVersion159() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // 说明卡片 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp) - ) { - Text( - text = stringResource(R.string.susfs_config_description), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = stringResource(R.string.susfs_config_description_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp - ) - } - } - - // Uname输入框 - OutlinedTextField( - value = unameValue, - onValueChange = onUnameValueChange, - label = { Text(stringResource(R.string.susfs_uname_label)) }, - placeholder = { Text(stringResource(R.string.susfs_uname_placeholder)) }, - modifier = Modifier.fillMaxWidth(), - enabled = !isLoading, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - // 构建时间伪装输入框 - OutlinedTextField( - value = buildTimeValue, - onValueChange = onBuildTimeValueChange, - label = { Text(stringResource(R.string.susfs_build_time_label)) }, - placeholder = { Text(stringResource(R.string.susfs_build_time_placeholder)) }, - modifier = Modifier.fillMaxWidth(), - enabled = !isLoading, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - // 执行位置选择 - ExposedDropdownMenuBox( - expanded = scriptLocationExpanded, - onExpandedChange = { scriptLocationExpanded = !scriptLocationExpanded } - ) { - OutlinedTextField( - value = if (executeInPostFsData) - stringResource(R.string.susfs_execution_location_post_fs_data) - else - stringResource(R.string.susfs_execution_location_service), - onValueChange = { }, - readOnly = true, - label = { Text(stringResource(R.string.susfs_execution_location_label)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = scriptLocationExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), - shape = RoundedCornerShape(8.dp), - enabled = !isLoading - ) - ExposedDropdownMenu( - expanded = scriptLocationExpanded, - onDismissRequest = { scriptLocationExpanded = false } - ) { - DropdownMenuItem( - text = { - Column { - Text(stringResource(R.string.susfs_execution_location_service)) - Text( - stringResource(R.string.susfs_execution_location_service_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - onClick = { - onExecuteInPostFsDataChange(false) - scriptLocationExpanded = false - } - ) - DropdownMenuItem( - text = { - Column { - Text(stringResource(R.string.susfs_execution_location_post_fs_data)) - Text( - stringResource(R.string.susfs_execution_location_post_fs_data_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - onClick = { - onExecuteInPostFsDataChange(true) - scriptLocationExpanded = false - } - ) - } - } - - // 当前值显示 - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(R.string.susfs_current_value, SuSFSManager.getUnameValue(context)), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.susfs_current_build_time, SuSFSManager.getBuildTimeValue(context)), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.susfs_current_execution_location, if (SuSFSManager.getExecuteInPostFsData(context)) "Post-FS-Data" else "Service"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // 开机自启动开关 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (canEnableAutoStart) { - MaterialTheme.colorScheme.surface - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - } - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.AutoMode, - contentDescription = null, - tint = if (canEnableAutoStart) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - }, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_autostart_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = if (canEnableAutoStart) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - } - ) - } - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = if (canEnableAutoStart) { - stringResource(R.string.susfs_autostart_description) - } else { - stringResource(R.string.susfs_autostart_requirement) - }, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy( - alpha = if (canEnableAutoStart) 1f else 0.5f - ), - lineHeight = 14.sp - ) - } - Switch( - checked = autoStartEnabled, - onCheckedChange = onAutoStartToggle, - enabled = !isLoading && canEnableAutoStart - ) - } - } - - // 隐藏BL脚本开关 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Security, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.hide_bl_script), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = stringResource(R.string.hide_bl_script_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - } - Switch( - checked = enableHideBl, - onCheckedChange = onEnableHideBlChange, - enabled = !isLoading - ) - } - } - - // 清理残留脚本开关 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.CleaningServices, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.cleanup_residue), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = stringResource(R.string.cleanup_residue_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - } - Switch( - checked = enableCleanupResidue, - onCheckedChange = onEnableCleanupResidueChange, - enabled = !isLoading - ) - } - } - - // AVC日志欺骗开关(仅在1.5.9+版本显示) - if (isSusVersion159) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.VisibilityOff, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.avc_log_spoofing), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = stringResource(R.string.avc_log_spoofing_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(R.string.avc_log_spoofing_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.secondary, - lineHeight = 12.sp - ) - } - Switch( - checked = enableAvcLogSpoofing, - onCheckedChange = onEnableAvcLogSpoofingChange, - enabled = !isLoading - ) - } - } - } - - // 槽位信息按钮 - if (isAbDevice) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_slot_info_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - Text( - text = stringResource(R.string.susfs_slot_info_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - - OutlinedButton( - onClick = onShowSlotInfo, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = Icons.Default.Storage, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_slot_info_title), - fontWeight = FontWeight.Medium - ) - } - } - } - } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 备份按钮 - OutlinedButton( - onClick = onShowBackupDialog, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .weight(1f) - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.Backup, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_backup_title), - fontWeight = FontWeight.Medium - ) - } - // 还原按钮 - OutlinedButton( - onClick = onShowRestoreDialog, - enabled = !isLoading, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .weight(1f) - .height(40.dp) - ) { - Icon( - imageVector = Icons.Default.Restore, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - stringResource(R.string.susfs_restore_title), - fontWeight = FontWeight.Medium - ) - } - } - } -} - -/** - * 槽位信息对话框 - */ -@Composable -private fun SlotInfoDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - slotInfoList: List, - currentActiveSlot: String, - isLoadingSlotInfo: Boolean, - onRefresh: () -> Unit, - onUseUname: (String) -> Unit, - onUseBuildTime: (String) -> Unit -) { - val isAbDevice = produceState(initialValue = false) { - value = isAbDevice() - }.value - - if (showDialog && isAbDevice) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(R.string.susfs_slot_info_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = stringResource(R.string.susfs_current_active_slot, currentActiveSlot), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - - if (slotInfoList.isNotEmpty()) { - slotInfoList.forEach { slotInfo -> - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (slotInfo.slotName == currentActiveSlot) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - } - ), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Storage, - contentDescription = null, - tint = if (slotInfo.slotName == currentActiveSlot) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = slotInfo.slotName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = if (slotInfo.slotName == currentActiveSlot) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - } - ) - if (slotInfo.slotName == currentActiveSlot) { - Spacer(modifier = Modifier.width(6.dp)) - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primary - ) { - Text( - text = stringResource(R.string.susfs_slot_current_badge), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) - } - } - } - Text( - text = stringResource(R.string.susfs_slot_uname, slotInfo.uname), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.susfs_slot_build_time, slotInfo.buildTime), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { onUseUname(slotInfo.uname) }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(6.dp) - ) { - Text(stringResource(R.string.susfs_slot_use_uname), fontSize = 12.sp) - } - Button( - onClick = { onUseBuildTime(slotInfo.buildTime) }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(6.dp) - ) { - Text(stringResource(R.string.susfs_slot_use_build_time), fontSize = 12.sp) - } - } - } - } - } - } else { - Text( - text = stringResource(R.string.susfs_slot_info_unavailable), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } - } - }, - confirmButton = { - Button( - onClick = onRefresh, - enabled = !isLoadingSlotInfo, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.refresh)) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.close)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt deleted file mode 100644 index 41a0c4ce..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt +++ /dev/null @@ -1,1733 +0,0 @@ -package com.sukisu.ultra.ui.susfs.component - -import android.annotation.SuppressLint -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.util.Log -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -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 coil.compose.AsyncImage -import coil.request.ImageRequest -import com.google.accompanist.drawablepainter.rememberDrawablePainter -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.susfs.util.SuSFSManager -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import kotlinx.coroutines.launch - -/** - * 添加路径对话框 - */ -@Composable -fun AddPathDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onConfirm: (String) -> Unit, - isLoading: Boolean, - titleRes: Int, - labelRes: Int, - placeholderRes: Int, - initialValue: String = "" -) { - var newPath by remember { mutableStateOf("") } - - // 当对话框显示时,设置初始值 - LaunchedEffect(showDialog, initialValue) { - if (showDialog) { - newPath = initialValue - } - } - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - stringResource(titleRes), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - OutlinedTextField( - value = newPath, - onValueChange = { newPath = it }, - label = { Text(stringResource(labelRes)) }, - placeholder = { Text(stringResource(placeholderRes)) }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) - }, - confirmButton = { - Button( - onClick = { - if (newPath.isNotBlank()) { - onConfirm(newPath.trim()) - newPath = "" - } - }, - enabled = newPath.isNotBlank() && !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(if (initialValue.isNotEmpty()) R.string.susfs_save else R.string.add)) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() - newPath = "" - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} - -/** - * 快捷添加应用路径对话框 - */ -@Composable -fun AddAppPathDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onConfirm: (List) -> Unit, - isLoading: Boolean, - apps: List = emptyList(), - onLoadApps: () -> Unit, - existingSusPaths: Set = emptySet() -) { - var searchText by remember { mutableStateOf("") } - var selectedApps by remember { mutableStateOf(setOf()) } - - // 获取已添加的包名 - val addedPackageNames = remember(existingSusPaths) { - existingSusPaths.mapNotNull { path -> - val regex = Regex(".*/Android/data/([^/]+)/?.*") - regex.find(path)?.groupValues?.get(1) - }.toSet() - } - - // 过滤掉已添加的应用 - val availableApps = remember(apps, addedPackageNames) { - apps.filter { app -> - !addedPackageNames.contains(app.packageName) - } - } - - val filteredApps = remember(availableApps, searchText) { - if (searchText.isBlank()) { - availableApps - } else { - availableApps.filter { app -> - app.appName.contains(searchText, ignoreCase = true) || - app.packageName.contains(searchText, ignoreCase = true) - } - } - } - - LaunchedEffect(showDialog) { - if (showDialog && apps.isEmpty()) { - onLoadApps() - } - // 当对话框显示时清空选择 - if (showDialog) { - selectedApps = setOf() - } - } - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(R.string.susfs_add_app_path), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = searchText, - onValueChange = { searchText = it }, - label = { Text(stringResource(R.string.search_apps)) }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null - ) - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) - - // 显示统计信息 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - if (selectedApps.isNotEmpty()) { - Text( - text = stringResource(R.string.selected_apps_count, selectedApps.size), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium - ) - } - if (addedPackageNames.isNotEmpty()) { - Text( - text = stringResource(R.string.already_added_apps_count, addedPackageNames.size), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - if (filteredApps.isEmpty()) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ) - ) { - Text( - text = if (availableApps.isEmpty()) { - stringResource(R.string.all_apps_already_added) - } else { - stringResource(R.string.no_apps_found) - }, - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - LazyColumn( - modifier = Modifier.height(300.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(filteredApps) { app -> - val isSelected = selectedApps.contains(app) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surface - } - ), - onClick = { - selectedApps = if (isSelected) { - selectedApps - app - } else { - selectedApps + app - } - } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 应用图标 - AppIcon( - packageName = app.packageName, - packageInfo = app.packageInfo, - modifier = Modifier.size(40.dp) - ) - - Column( - modifier = Modifier - .weight(1f) - .padding(start = 12.dp) - ) { - Text( - text = app.appName, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurface - } - ) - Text( - text = app.packageName, - style = MaterialTheme.typography.bodyMedium, - color = if (isSelected) { - MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } - - // 选择指示器 - if (isSelected) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } else { - Icon( - imageVector = Icons.Default.RadioButtonUnchecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } - } - } - } - } - } - } - }, - confirmButton = { - Button( - onClick = { - if (selectedApps.isNotEmpty()) { - onConfirm(selectedApps.map { it.packageName }) - } - selectedApps = setOf() - searchText = "" - }, - enabled = selectedApps.isNotEmpty() && !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = stringResource(R.string.add) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() - selectedApps = setOf() - searchText = "" - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} - - -/** - * 应用图标组件 - */ -@Composable -fun AppIcon( - packageName: String, - packageInfo: PackageInfo? = null, - @SuppressLint("ModifierParameter") modifier: Modifier = Modifier -) { - val context = LocalContext.current - if (packageInfo != null) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(packageInfo) - .crossfade(true) - .build(), - contentDescription = null, - modifier = modifier.clip(RoundedCornerShape(8.dp)) - ) - } else { - var appIcon by remember(packageName) { - mutableStateOf( - AppInfoCache.getAppInfo(packageName)?.drawable - ) - } - - LaunchedEffect(packageName) { - if (appIcon == null && !AppInfoCache.hasCache(packageName)) { - try { - val packageManager = context.packageManager - val applicationInfo = packageManager.getApplicationInfo(packageName, 0) - val drawable = packageManager.getApplicationIcon(applicationInfo) - appIcon = drawable - val cachedInfo = AppInfoCache.CachedAppInfo( - appName = packageName, - packageInfo = null, - drawable = drawable - ) - AppInfoCache.putAppInfo(packageName, cachedInfo) - } catch (_: Exception) { - Log.d("获取应用图标失败", packageName) - } - } - } - Image( - painter = rememberDrawablePainter(appIcon), - contentDescription = null, - modifier = modifier.clip(RoundedCornerShape(8.dp)) - ) - } -} - - -/** - * 添加尝试卸载对话框 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddTryUmountDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onConfirm: (String, Int) -> Unit, - isLoading: Boolean, - initialPath: String = "", - initialMode: Int = 0 -) { - var newUmountPath by remember { mutableStateOf("") } - var newUmountMode by remember { mutableIntStateOf(0) } - var umountModeExpanded by remember { mutableStateOf(false) } - - // 当对话框显示时,设置初始值 - LaunchedEffect(showDialog, initialPath, initialMode) { - if (showDialog) { - newUmountPath = initialPath - newUmountMode = initialMode - } - } - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - stringResource(if (initialPath.isNotEmpty()) R.string.susfs_edit_try_umount else R.string.susfs_add_try_umount), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = newUmountPath, - onValueChange = { newUmountPath = it }, - label = { Text(stringResource(R.string.susfs_path_label)) }, - placeholder = { Text(stringResource(R.string.susfs_path_placeholder)) }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) - - ExposedDropdownMenuBox( - expanded = umountModeExpanded, - onExpandedChange = { umountModeExpanded = !umountModeExpanded } - ) { - OutlinedTextField( - value = if (newUmountMode == 0) - stringResource(R.string.susfs_umount_mode_normal) - else - stringResource(R.string.susfs_umount_mode_detach), - onValueChange = { }, - readOnly = true, - label = { Text(stringResource(R.string.susfs_umount_mode_label)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), - shape = RoundedCornerShape(8.dp) - ) - ExposedDropdownMenu( - expanded = umountModeExpanded, - onDismissRequest = { umountModeExpanded = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.susfs_umount_mode_normal)) }, - onClick = { - newUmountMode = 0 - umountModeExpanded = false - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.susfs_umount_mode_detach)) }, - onClick = { - newUmountMode = 1 - umountModeExpanded = false - } - ) - } - } - } - }, - confirmButton = { - Button( - onClick = { - if (newUmountPath.isNotBlank()) { - onConfirm(newUmountPath.trim(), newUmountMode) - newUmountPath = "" - newUmountMode = 0 - } - }, - enabled = newUmountPath.isNotBlank() && !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(if (initialPath.isNotEmpty()) R.string.susfs_save else R.string.add)) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() - newUmountPath = "" - newUmountMode = 0 - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} - -/** - * 添加Kstat静态配置对话框 - */ -@Composable -fun AddKstatStaticallyDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onConfirm: (String, String, String, String, String, String, String, String, String, String, String, String, String) -> Unit, - isLoading: Boolean, - initialConfig: String = "" -) { - var newKstatPath by remember { mutableStateOf("") } - var newKstatIno by remember { mutableStateOf("") } - var newKstatDev by remember { mutableStateOf("") } - var newKstatNlink by remember { mutableStateOf("") } - var newKstatSize by remember { mutableStateOf("") } - var newKstatAtime by remember { mutableStateOf("") } - var newKstatAtimeNsec by remember { mutableStateOf("") } - var newKstatMtime by remember { mutableStateOf("") } - var newKstatMtimeNsec by remember { mutableStateOf("") } - var newKstatCtime by remember { mutableStateOf("") } - var newKstatCtimeNsec by remember { mutableStateOf("") } - var newKstatBlocks by remember { mutableStateOf("") } - var newKstatBlksize by remember { mutableStateOf("") } - - // 当对话框显示时,解析初始配置 - LaunchedEffect(showDialog, initialConfig) { - if (showDialog && initialConfig.isNotEmpty()) { - val parts = initialConfig.split("|") - if (parts.size >= 13) { - newKstatPath = parts[0] - newKstatIno = if (parts[1] == "default") "" else parts[1] - newKstatDev = if (parts[2] == "default") "" else parts[2] - newKstatNlink = if (parts[3] == "default") "" else parts[3] - newKstatSize = if (parts[4] == "default") "" else parts[4] - newKstatAtime = if (parts[5] == "default") "" else parts[5] - newKstatAtimeNsec = if (parts[6] == "default") "" else parts[6] - newKstatMtime = if (parts[7] == "default") "" else parts[7] - newKstatMtimeNsec = if (parts[8] == "default") "" else parts[8] - newKstatCtime = if (parts[9] == "default") "" else parts[9] - newKstatCtimeNsec = if (parts[10] == "default") "" else parts[10] - newKstatBlocks = if (parts[11] == "default") "" else parts[11] - newKstatBlksize = if (parts[12] == "default") "" else parts[12] - } - } else if (showDialog && initialConfig.isEmpty()) { - // 清空所有字段 - newKstatPath = "" - newKstatIno = "" - newKstatDev = "" - newKstatNlink = "" - newKstatSize = "" - newKstatAtime = "" - newKstatAtimeNsec = "" - newKstatMtime = "" - newKstatMtimeNsec = "" - newKstatCtime = "" - newKstatCtimeNsec = "" - newKstatBlocks = "" - newKstatBlksize = "" - } - } - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - stringResource(if (initialConfig.isNotEmpty()) R.string.edit_kstat_statically_title else R.string.add_kstat_statically_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatPath, - onValueChange = { newKstatPath = it }, - label = { Text(stringResource(R.string.file_or_directory_path_label)) }, - placeholder = { Text("/path/to/file_or_directory") }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatIno, - onValueChange = { newKstatIno = it }, - label = { Text("ino") }, - placeholder = { Text("1234") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatDev, - onValueChange = { newKstatDev = it }, - label = { Text("dev") }, - placeholder = { Text("1234") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatNlink, - onValueChange = { newKstatNlink = it }, - label = { Text("nlink") }, - placeholder = { Text("2") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatSize, - onValueChange = { newKstatSize = it }, - label = { Text("size") }, - placeholder = { Text("223344") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatAtime, - onValueChange = { newKstatAtime = it }, - label = { Text("atime") }, - placeholder = { Text("1712592355") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatAtimeNsec, - onValueChange = { newKstatAtimeNsec = it }, - label = { Text("atime_nsec") }, - placeholder = { Text("0") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatMtime, - onValueChange = { newKstatMtime = it }, - label = { Text("mtime") }, - placeholder = { Text("1712592355") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatMtimeNsec, - onValueChange = { newKstatMtimeNsec = it }, - label = { Text("mtime_nsec") }, - placeholder = { Text("0") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatCtime, - onValueChange = { newKstatCtime = it }, - label = { Text("ctime") }, - placeholder = { Text("1712592355") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatCtimeNsec, - onValueChange = { newKstatCtimeNsec = it }, - label = { Text("ctime_nsec") }, - placeholder = { Text("0") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = newKstatBlocks, - onValueChange = { newKstatBlocks = it }, - label = { Text("blocks") }, - placeholder = { Text("16") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - OutlinedTextField( - value = newKstatBlksize, - onValueChange = { newKstatBlksize = it }, - label = { Text("blksize") }, - placeholder = { Text("512") }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(8.dp) - ) - } - - Text( - text = stringResource(R.string.hint_use_default_value), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - confirmButton = { - Button( - onClick = { - if (newKstatPath.isNotBlank()) { - onConfirm( - newKstatPath.trim(), - newKstatIno.trim().ifBlank { "default" }, - newKstatDev.trim().ifBlank { "default" }, - newKstatNlink.trim().ifBlank { "default" }, - newKstatSize.trim().ifBlank { "default" }, - newKstatAtime.trim().ifBlank { "default" }, - newKstatAtimeNsec.trim().ifBlank { "default" }, - newKstatMtime.trim().ifBlank { "default" }, - newKstatMtimeNsec.trim().ifBlank { "default" }, - newKstatCtime.trim().ifBlank { "default" }, - newKstatCtimeNsec.trim().ifBlank { "default" }, - newKstatBlocks.trim().ifBlank { "default" }, - newKstatBlksize.trim().ifBlank { "default" } - ) - // 清空所有字段 - newKstatPath = "" - newKstatIno = "" - newKstatDev = "" - newKstatNlink = "" - newKstatSize = "" - newKstatAtime = "" - newKstatAtimeNsec = "" - newKstatMtime = "" - newKstatMtimeNsec = "" - newKstatCtime = "" - newKstatCtimeNsec = "" - newKstatBlocks = "" - newKstatBlksize = "" - } - }, - enabled = newKstatPath.isNotBlank() && !isLoading, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(if (initialConfig.isNotEmpty()) R.string.susfs_save else R.string.add)) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() - // 清空所有字段 - newKstatPath = "" - newKstatIno = "" - newKstatDev = "" - newKstatNlink = "" - newKstatSize = "" - newKstatAtime = "" - newKstatAtimeNsec = "" - newKstatMtime = "" - newKstatMtimeNsec = "" - newKstatCtime = "" - newKstatCtimeNsec = "" - newKstatBlocks = "" - newKstatBlksize = "" - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} - -/** - * 确认对话框 - */ -@Composable -fun ConfirmDialog( - showDialog: Boolean, - onDismiss: () -> Unit, - onConfirm: () -> Unit, - titleRes: Int, - messageRes: Int, - isLoading: Boolean = false, - isDestructive: Boolean = false -) { - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(titleRes), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { Text(stringResource(messageRes)) }, - confirmButton = { - Button( - onClick = onConfirm, - enabled = !isLoading, - colors = if (isDestructive) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - } else { - ButtonDefaults.buttonColors() - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } -} - -// 应用信息缓存 -object AppInfoCache { - private val appInfoMap = mutableMapOf() - - data class CachedAppInfo( - val appName: String, - val packageInfo: PackageInfo?, - val drawable: Drawable?, - val timestamp: Long = System.currentTimeMillis() - ) - - fun getAppInfo(packageName: String): CachedAppInfo? { - return appInfoMap[packageName] - } - - fun putAppInfo(packageName: String, appInfo: CachedAppInfo) { - appInfoMap[packageName] = appInfo - } - - fun clearCache() { - appInfoMap.clear() - } - - fun hasCache(packageName: String): Boolean { - return appInfoMap.containsKey(packageName) - } - - fun getAppInfoFromSuperUser(packageName: String): CachedAppInfo? { - val superUserApp = SuperUserViewModel.apps.find { it.packageName == packageName } - return superUserApp?.let { app -> - CachedAppInfo( - appName = app.label, - packageInfo = app.packageInfo, - drawable = null - ) - } - } -} - -/** - * 空状态显示组件 - */ -@Composable -fun EmptyStateCard( - message: String, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - } -} - -/** - * 路径项目卡片组件 - */ -@Composable -fun PathItemCard( - path: String, - icon: ImageVector, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - isLoading: Boolean = false, - additionalInfo: String? = null -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - Text( - text = path, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (additionalInfo != null) { - Text( - text = additionalInfo, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * Kstat配置项目卡片组件 - */ -@Composable -fun KstatConfigItemCard( - config: String, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - isLoading: Boolean = false -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column { - val parts = config.split("|") - if (parts.isNotEmpty()) { - Text( - text = parts[0], // 路径 - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (parts.size > 1) { - Text( - text = parts.drop(1).joinToString(" "), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else { - Text( - text = config, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - } - } - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * Add Kstat路径项目卡片组件 - */ -@Composable -fun AddKstatPathItemCard( - path: String, - onDelete: () -> Unit, - onEdit: (() -> Unit)? = null, - onUpdate: () -> Unit, - onUpdateFullClone: () -> Unit, - isLoading: Boolean = false -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 1.dp), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Folder, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = path, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEdit != null) { - IconButton( - onClick = onEdit, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - } - IconButton( - onClick = onUpdate, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Update, - contentDescription = stringResource(R.string.update), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(16.dp) - ) - } - IconButton( - onClick = onUpdateFullClone, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = stringResource(R.string.susfs_update_full_clone), - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.size(16.dp) - ) - } - IconButton( - onClick = onDelete, - enabled = !isLoading, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - } - } - } -} - -/** - * 启用功能状态卡片组件 - */ -@Composable -fun FeatureStatusCard( - feature: SuSFSManager.EnabledFeature, - onRefresh: (() -> Unit)? = null, - @SuppressLint("ModifierParameter") modifier: Modifier = Modifier -) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - - // 日志配置对话框状态 - var showLogConfigDialog by remember { mutableStateOf(false) } - var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) } - - // 日志配置对话框 - if (showLogConfigDialog) { - AlertDialog( - onDismissRequest = { showLogConfigDialog = false }, - title = { - Text( - text = stringResource(R.string.susfs_log_config_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = stringResource(R.string.susfs_log_config_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.susfs_enable_log_label), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - Switch( - checked = logEnabled, - onCheckedChange = { logEnabled = it } - ) - } - } - }, - confirmButton = { - Button( - onClick = { - coroutineScope.launch { - if (SuSFSManager.setEnableLog(context, logEnabled)) { - onRefresh?.invoke() - } - showLogConfigDialog = false - } - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_apply)) - } - }, - dismissButton = { - TextButton( - onClick = { - // 恢复原始状态 - logEnabled = SuSFSManager.getEnableLogState(context) - showLogConfigDialog = false - }, - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.cancel)) - } - }, - shape = RoundedCornerShape(12.dp) - ) - } - - Card( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 1.dp) - .then( - if (feature.canConfigure) { - Modifier.clickable { - // 更新当前状态 - logEnabled = SuSFSManager.getEnableLogState(context) - showLogConfigDialog = true - } - } else { - Modifier - } - ), - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = feature.name, - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium - ) - if (feature.canConfigure) { - Text( - text = stringResource(R.string.susfs_feature_configurable), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 状态标签 - Surface( - shape = RoundedCornerShape(6.dp), - color = when { - feature.isEnabled -> MaterialTheme.colorScheme.primary - else -> Color.Gray - } - ) { - Text( - text = feature.statusText, - style = MaterialTheme.typography.labelLarge, - color = when { - feature.isEnabled -> MaterialTheme.colorScheme.onPrimary - else -> Color.White - }, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp) - ) - } - } - } - } -} - -/** - * SUS挂载隐藏控制卡片组件 - */ -@Composable -fun SusMountHidingControlCard( - hideSusMountsForAllProcs: Boolean, - isLoading: Boolean, - onToggleHiding: (Boolean) -> Unit -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 标题行 - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_hide_mounts_control_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - } - - // 描述文本 - Text( - text = stringResource(R.string.susfs_hide_mounts_control_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp - ) - - // 控制开关行 - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = if (hideSusMountsForAllProcs) { - stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description) - } else { - stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - } - Switch( - checked = hideSusMountsForAllProcs, - onCheckedChange = onToggleHiding, - enabled = !isLoading - ) - } - - // 当前设置显示 - Text( - text = stringResource( - R.string.susfs_hide_mounts_current_setting, - if (hideSusMountsForAllProcs) { - stringResource(R.string.susfs_hide_mounts_setting_all) - } else { - stringResource(R.string.susfs_hide_mounts_setting_non_ksu) - } - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Medium - ) - - // 建议文本 - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = stringResource(R.string.susfs_hide_mounts_recommendation), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp, - modifier = Modifier.padding(12.dp) - ) - } - } - } -} - -/** - * 应用路径分组卡片 - */ -@Composable -fun AppPathGroupCard( - packageName: String, - paths: List, - onDeleteGroup: () -> Unit, - onEditGroup: (() -> Unit)? = null, - isLoading: Boolean -) { - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val superUserApps = SuperUserViewModel.apps - var cachedAppInfo by remember(packageName, superUserApps.size) { - mutableStateOf(AppInfoCache.getAppInfo(packageName)) - } - var isLoadingAppInfo by remember(packageName, superUserApps.size) { mutableStateOf(false) } - - LaunchedEffect(packageName, superUserApps.size) { - if (cachedAppInfo == null || superUserApps.isNotEmpty()) { - isLoadingAppInfo = true - coroutineScope.launch { - try { - val superUserAppInfo = AppInfoCache.getAppInfoFromSuperUser(packageName) - - if (superUserAppInfo != null) { - val packageManager = context.packageManager - val drawable = try { - superUserAppInfo.packageInfo?.applicationInfo?.let { - packageManager.getApplicationIcon(it) - } - } catch (_: Exception) { - null - } - - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = superUserAppInfo.appName, - packageInfo = superUserAppInfo.packageInfo, - drawable = drawable - ) - - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } else { - val packageManager = context.packageManager - val appInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) - - val appName = try { - appInfo.applicationInfo?.let { - packageManager.getApplicationLabel(it).toString() - } ?: packageName - } catch (_: Exception) { - packageName - } - - val drawable = try { - appInfo.applicationInfo?.let { - packageManager.getApplicationIcon(it) - } - } catch (_: Exception) { - null - } - - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = appName, - packageInfo = appInfo, - drawable = drawable - ) - - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } - } catch (_: Exception) { - val newCachedInfo = AppInfoCache.CachedAppInfo( - appName = packageName, - packageInfo = null, - drawable = null - ) - AppInfoCache.putAppInfo(packageName, newCachedInfo) - cachedAppInfo = newCachedInfo - } finally { - isLoadingAppInfo = false - } - } - } - } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - // 应用图标 - AppIcon( - packageName = packageName, - packageInfo = cachedAppInfo?.packageInfo, - modifier = Modifier.size(32.dp) - ) - - Spacer(modifier = Modifier.width(12.dp)) - - Column(modifier = Modifier.weight(1f)) { - val displayName = cachedAppInfo?.appName?.ifEmpty { packageName } ?: packageName - Text( - text = displayName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - if (!isLoadingAppInfo && cachedAppInfo?.appName?.isNotEmpty() == true && - cachedAppInfo?.appName != packageName) { - Text( - text = packageName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (onEditGroup != null) { - IconButton( - onClick = onEditGroup, - enabled = !isLoading - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(R.string.edit), - tint = MaterialTheme.colorScheme.primary - ) - } - } - IconButton( - onClick = onDeleteGroup, - enabled = !isLoading - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete), - tint = MaterialTheme.colorScheme.error - ) - } - } - } - - // 显示所有路径 - Spacer(modifier = Modifier.height(8.dp)) - - paths.forEach { path -> - Text( - text = path, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), - RoundedCornerShape(6.dp) - ) - .padding(8.dp) - ) - - if (path != paths.last()) { - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - } -} - -/** - * 分组标题组件 - */ -@Composable -fun SectionHeader( - title: String, - subtitle: String?, - icon: ImageVector, - count: Int -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - subtitle?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Surface( - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.primary - ) { - Text( - text = count.toString(), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimary, - fontWeight = FontWeight.Bold - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt deleted file mode 100644 index be683f23..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt +++ /dev/null @@ -1,928 +0,0 @@ -package com.sukisu.ultra.ui.susfs.component - -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.susfs.util.SuSFSManager -import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel - -/** - * SUS路径内容组件 - */ -@Composable -fun SusPathsContent( - susPaths: Set, - isLoading: Boolean, - onAddPath: () -> Unit, - onAddAppPath: () -> Unit, - onRemovePath: (String) -> Unit, - onEditPath: ((String) -> Unit)? = null, - forceRefreshApps: Boolean = false -) { - val superUserApps = SuperUserViewModel.apps - val superUserIsRefreshing = remember { SuperUserViewModel().isRefreshing } - - LaunchedEffect(superUserIsRefreshing, superUserApps.size) { - if (!superUserIsRefreshing && superUserApps.isNotEmpty()) { - AppInfoCache.clearCache() - } - } - - LaunchedEffect(forceRefreshApps) { - if (forceRefreshApps) { - AppInfoCache.clearCache() - } - } - - val (appPathGroups, otherPaths) = remember(susPaths) { - val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*") - val uidPathRegex = Regex("/sys/fs/cgroup/uid_([0-9]+)") - val appPathMap = mutableMapOf>() - val uidToPackageMap = mutableMapOf() - val others = mutableListOf() - - // 构建UID到包名的映射 - SuperUserViewModel.apps.forEach { app -> - try { - val uid = app.packageInfo.applicationInfo?.uid - uidToPackageMap[uid.toString()] = app.packageName - } catch (_: Exception) { - } - } - - susPaths.forEach { path -> - val appDataMatch = appPathRegex.find(path) - val uidMatch = uidPathRegex.find(path) - - when { - appDataMatch != null -> { - val packageName = appDataMatch.groupValues[1] - appPathMap.getOrPut(packageName) { mutableListOf() }.add(path) - } - uidMatch != null -> { - val uid = uidMatch.groupValues[1] - val packageName = uidToPackageMap[uid] - if (packageName != null) { - appPathMap.getOrPut(packageName) { mutableListOf() }.add(path) - } else { - others.add(path) - } - } - else -> { - others.add(path) - } - } - } - - val sortedAppGroups = appPathMap.toList() - .sortedBy { it.first } - .map { (packageName, paths) -> packageName to paths.sorted() } - - Pair(sortedAppGroups, others.sorted()) - } - - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 应用路径分组 - if (appPathGroups.isNotEmpty()) { - item { - SectionHeader( - title = stringResource(R.string.app_paths_section), - subtitle = null, - icon = Icons.Default.Apps, - count = appPathGroups.size - ) - } - - items(appPathGroups) { (packageName, paths) -> - AppPathGroupCard( - packageName = packageName, - paths = paths, - onDeleteGroup = { - paths.forEach { path -> onRemovePath(path) } - }, - onEditGroup = if (onEditPath != null) { - { - onEditPath(paths.first()) - } - } else null, - isLoading = isLoading - ) - } - } - - // 其他路径 - if (otherPaths.isNotEmpty()) { - item { - SectionHeader( - title = stringResource(R.string.other_paths_section), - subtitle = null, - icon = Icons.Default.Folder, - count = otherPaths.size - ) - } - - items(otherPaths) { path -> - PathItemCard( - path = path, - icon = Icons.Default.Folder, - onDelete = { onRemovePath(path) }, - onEdit = if (onEditPath != null) { { onEditPath(path) } } else null, - isLoading = isLoading - ) - } - } - - if (susPaths.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_paths_configured) - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddPath, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add_custom_path)) - } - - Button( - onClick = onAddAppPath, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Apps, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add_app_path)) - } - } - } - } - } -} - -/** - * SUS循环路径内容组件 - */ -@Composable -fun SusLoopPathsContent( - susLoopPaths: Set, - isLoading: Boolean, - onAddLoopPath: () -> Unit, - onRemoveLoopPath: (String) -> Unit, - onEditLoopPath: ((String) -> Unit)? = null -) { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 说明卡片 - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = stringResource(R.string.sus_loop_paths_description_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(R.string.sus_loop_paths_description_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.susfs_loop_path_restriction_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.secondary - ) - } - } - } - - if (susLoopPaths.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_loop_paths_configured) - ) - } - } else { - item { - SectionHeader( - title = stringResource(R.string.loop_paths_section), - subtitle = null, - icon = Icons.Default.Loop, - count = susLoopPaths.size - ) - } - - items(susLoopPaths.toList()) { path -> - PathItemCard( - path = path, - icon = Icons.Default.Loop, - onDelete = { onRemoveLoopPath(path) }, - onEdit = if (onEditLoopPath != null) { { onEditLoopPath(path) } } else null, - isLoading = isLoading - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddLoopPath, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add_loop_path)) - } - } - } - } - } -} - -/** - * SUS Maps内容组件 - */ -@Composable -fun SusMapsContent( - susMaps: Set, - isLoading: Boolean, - onAddSusMap: () -> Unit, - onRemoveSusMap: (String) -> Unit, - onEditSusMap: ((String) -> Unit)? = null -) { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // 说明卡片 - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = stringResource(R.string.sus_maps_description_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(R.string.sus_maps_description_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.sus_maps_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.secondary - ) - Text( - text = stringResource(R.string.sus_maps_debug_info), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - } - } - - if (susMaps.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_sus_maps_configured) - ) - } - } else { - item { - SectionHeader( - title = stringResource(R.string.sus_maps_section), - subtitle = null, - icon = Icons.Default.Security, - count = susMaps.size - ) - } - - items(susMaps.toList()) { map -> - PathItemCard( - path = map, - icon = Icons.Default.Security, - onDelete = { onRemoveSusMap(map) }, - onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null, - isLoading = isLoading - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddSusMap, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) - } - } - } - } - } -} - -/** - * SUS挂载内容组件 - */ -@Composable -fun SusMountsContent( - susMounts: Set, - hideSusMountsForAllProcs: Boolean, - isSusVersion158: Boolean, - isLoading: Boolean, - onAddMount: () -> Unit, - onRemoveMount: (String) -> Unit, - onEditMount: ((String) -> Unit)? = null, - onToggleHideSusMountsForAllProcs: (Boolean) -> Unit -) { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (isSusVersion158) { - item { - SusMountHidingControlCard( - hideSusMountsForAllProcs = hideSusMountsForAllProcs, - isLoading = isLoading, - onToggleHiding = onToggleHideSusMountsForAllProcs - ) - } - } - - if (susMounts.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_mounts_configured) - ) - } - } else { - items(susMounts.toList()) { mount -> - PathItemCard( - path = mount, - icon = Icons.Default.Storage, - onDelete = { onRemoveMount(mount) }, - onEdit = if (onEditMount != null) { { onEditMount(mount) } } else null, - isLoading = isLoading - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddMount, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) - } - } - } - } - } -} - -/** - * 尝试卸载内容组件 - */ -@Composable -fun TryUmountContent( - tryUmounts: Set, - umountForZygoteIsoService: Boolean, - isLoading: Boolean, - onAddUmount: () -> Unit, - onRemoveUmount: (String) -> Unit, - onEditUmount: ((String) -> Unit)? = null, - onToggleUmountForZygoteIsoService: (Boolean) -> Unit -) { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (isSusVersion158()) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Security, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.umount_zygote_iso_service), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(modifier = Modifier.height(6.dp)) - Text( - text = stringResource(R.string.umount_zygote_iso_service_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 14.sp - ) - } - Switch( - checked = umountForZygoteIsoService, - onCheckedChange = onToggleUmountForZygoteIsoService, - enabled = !isLoading - ) - } - } - } - } - - if (tryUmounts.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_umounts_configured) - ) - } - } else { - items(tryUmounts.toList()) { umountEntry -> - val parts = umountEntry.split("|") - val path = if (parts.isNotEmpty()) parts[0] else umountEntry - val mode = if (parts.size > 1) parts[1] else "0" - val modeText = if (mode == "0") - stringResource(R.string.susfs_umount_mode_normal_short) - else - stringResource(R.string.susfs_umount_mode_detach_short) - - PathItemCard( - path = path, - icon = Icons.Default.Storage, - additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode), - onDelete = { onRemoveUmount(umountEntry) }, - onEdit = if (onEditUmount != null) { { onEditUmount(umountEntry) } } else null, - isLoading = isLoading - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddUmount, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) - } - } - } - } - } -} - -/** - * Kstat配置内容组件 - */ -@Composable -fun KstatConfigContent( - kstatConfigs: Set, - addKstatPaths: Set, - isLoading: Boolean, - onAddKstatStatically: () -> Unit, - onAddKstat: () -> Unit, - onRemoveKstatConfig: (String) -> Unit, - onEditKstatConfig: ((String) -> Unit)? = null, - onRemoveAddKstat: (String) -> Unit, - onEditAddKstat: ((String) -> Unit)? = null, - onUpdateKstat: (String) -> Unit, - onUpdateKstatFullClone: (String) -> Unit -) { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = stringResource(R.string.kstat_config_description_title), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(R.string.kstat_config_description_add_statically), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.kstat_config_description_add), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.kstat_config_description_update), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = stringResource(R.string.kstat_config_description_update_full_clone), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - if (kstatConfigs.isNotEmpty()) { - item { - Text( - text = stringResource(R.string.static_kstat_config), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - items(kstatConfigs.toList()) { config -> - KstatConfigItemCard( - config = config, - onDelete = { onRemoveKstatConfig(config) }, - onEdit = if (onEditKstatConfig != null) { { onEditKstatConfig(config) } } else null, - isLoading = isLoading - ) - } - } - - if (addKstatPaths.isNotEmpty()) { - item { - Text( - text = stringResource(R.string.kstat_path_management), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - items(addKstatPaths.toList()) { path -> - AddKstatPathItemCard( - path = path, - onDelete = { onRemoveAddKstat(path) }, - onEdit = if (onEditAddKstat != null) { { onEditAddKstat(path) } } else null, - onUpdate = { onUpdateKstat(path) }, - onUpdateFullClone = { onUpdateKstatFullClone(path) }, - isLoading = isLoading - ) - } - } - - if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.no_kstat_config_message) - ) - } - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = onAddKstat, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) - } - - Button( - onClick = onAddKstatStatically, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.add)) - } - } - } - } - } -} - -/** - * 路径设置内容组件 - */ -@SuppressLint("SdCardPath") -@Composable -fun PathSettingsContent( - androidDataPath: String, - onAndroidDataPathChange: (String) -> Unit, - sdcardPath: String, - onSdcardPathChange: (String) -> Unit, - isLoading: Boolean, - onSetAndroidDataPath: () -> Unit, - onSetSdcardPath: () -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = androidDataPath, - onValueChange = onAndroidDataPathChange, - label = { Text(stringResource(R.string.susfs_android_data_path_label)) }, - placeholder = { Text("/sdcard/Android/data") }, - modifier = Modifier.fillMaxWidth(), - enabled = !isLoading, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - Button( - onClick = onSetAndroidDataPath, - enabled = !isLoading && androidDataPath.isNotBlank(), - modifier = Modifier - .fillMaxWidth() - .height(40.dp), - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_set_android_data_path)) - } - } - } - } - - item { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = sdcardPath, - onValueChange = onSdcardPathChange, - label = { Text(stringResource(R.string.susfs_sdcard_path_label)) }, - placeholder = { Text("/sdcard") }, - modifier = Modifier.fillMaxWidth(), - enabled = !isLoading, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - Button( - onClick = onSetSdcardPath, - enabled = !isLoading && sdcardPath.isNotBlank(), - modifier = Modifier - .fillMaxWidth() - .height(40.dp), - shape = RoundedCornerShape(8.dp) - ) { - Text(stringResource(R.string.susfs_set_sdcard_path)) - } - } - } - } - } -} - -/** - * 启用功能状态内容组件 - */ -@Composable -fun EnabledFeaturesContent( - enabledFeatures: List, - onRefresh: () -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Column( - modifier = Modifier.padding(12.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.susfs_enabled_features_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - if (enabledFeatures.isEmpty()) { - item { - EmptyStateCard( - message = stringResource(R.string.susfs_no_features_found) - ) - } - } else { - items(enabledFeatures) { feature -> - FeatureStatusCard( - feature = feature, - onRefresh = onRefresh - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt deleted file mode 100644 index 9dca4cb3..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt +++ /dev/null @@ -1,1526 +0,0 @@ -package com.sukisu.ultra.ui.susfs.util - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import android.os.Build -import android.util.Log -import android.widget.Toast -import com.dergoogler.mmrl.platform.Platform.Companion.context -import com.sukisu.ultra.R -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import androidx.core.content.edit -import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.getSuSFSVersion -import com.sukisu.ultra.ui.util.getSuSFSFeatures -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import org.json.JSONArray -import org.json.JSONObject -import java.text.SimpleDateFormat -import java.util.* - -/** - * SuSFS 配置管理器 - * 用于管理SuSFS相关的配置和命令执行 - */ -object SuSFSManager { - private const val PREFS_NAME = "susfs_config" - private const val KEY_UNAME_VALUE = "uname_value" - private const val KEY_BUILD_TIME_VALUE = "build_time_value" - private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" - private const val KEY_SUS_PATHS = "sus_paths" - private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" - - private const val KEY_SUS_MAPS = "sus_maps" - private const val KEY_SUS_MOUNTS = "sus_mounts" - private const val KEY_TRY_UMOUNTS = "try_umounts" - private const val KEY_ANDROID_DATA_PATH = "android_data_path" - private const val KEY_SDCARD_PATH = "sdcard_path" - private const val KEY_ENABLE_LOG = "enable_log" - private const val KEY_EXECUTE_IN_POST_FS_DATA = "execute_in_post_fs_data" - private const val KEY_KSTAT_CONFIGS = "kstat_configs" - private const val KEY_ADD_KSTAT_PATHS = "add_kstat_paths" - private const val KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS = "hide_sus_mounts_for_all_procs" - private const val KEY_ENABLE_CLEANUP_RESIDUE = "enable_cleanup_residue" - private const val KEY_ENABLE_HIDE_BL = "enable_hide_bl" - private const val KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE = "umount_for_zygote_iso_service" - private const val KEY_ENABLE_AVC_LOG_SPOOFING = "enable_avc_log_spoofing" - - - // 常量 - private const val SUSFS_BINARY_TARGET_NAME = "ksu_susfs" - private const val DEFAULT_UNAME = "default" - private const val DEFAULT_BUILD_TIME = "default" - private const val MODULE_ID = "susfs_manager" - private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" - private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" - private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9" - private const val MIN_VERSION_SUS_MAPS = "1.5.12" - const val MAX_SUSFS_VERSION = "2.0.0" - private const val BACKUP_FILE_EXTENSION = ".susfs_backup" - private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" - private const val CGROUP_UID_PATH_PREFIX = "/sys/fs/cgroup/uid_" - - data class SlotInfo(val slotName: String, val uname: String, val buildTime: String) - data class CommandResult(val isSuccess: Boolean, val output: String, val errorOutput: String = "") - data class EnabledFeature( - val name: String, - val isEnabled: Boolean, - val statusText: String = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled), - val canConfigure: Boolean = false - ) - - /** - * 应用信息数据类 - */ - data class AppInfo( - val packageName: String, - val appName: String, - val packageInfo: PackageInfo, - val isSystemApp: Boolean - ) - - /** - * 备份数据类 - */ - data class BackupData( - val version: String, - val timestamp: Long, - val deviceInfo: String, - val configurations: Map - ) { - fun toJson(): String { - val jsonObject = JSONObject().apply { - put("version", version) - put("timestamp", timestamp) - put("deviceInfo", deviceInfo) - put("configurations", JSONObject(configurations)) - } - return jsonObject.toString(2) - } - - companion object { - fun fromJson(jsonString: String): BackupData? { - return try { - val jsonObject = JSONObject(jsonString) - val configurationsJson = jsonObject.getJSONObject("configurations") - val configurations = mutableMapOf() - - configurationsJson.keys().forEach { key -> - val value = configurationsJson.get(key) - configurations[key] = when (value) { - is JSONArray -> { - val set = mutableSetOf() - for (i in 0 until value.length()) { - set.add(value.getString(i)) - } - set - } - else -> value - } - } - - BackupData( - version = jsonObject.getString("version"), - timestamp = jsonObject.getLong("timestamp"), - deviceInfo = jsonObject.getString("deviceInfo"), - configurations = configurations - ) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - } - } - - /** - * 模块配置数据类 - */ - data class ModuleConfig( - val targetPath: String, - val unameValue: String, - val buildTimeValue: String, - val executeInPostFsData: Boolean, - val susPaths: Set, - val susLoopPaths: Set, - val susMaps: Set, - val susMounts: Set, - val tryUmounts: Set, - val androidDataPath: String, - val sdcardPath: String, - val enableLog: Boolean, - val kstatConfigs: Set, - val addKstatPaths: Set, - val hideSusMountsForAllProcs: Boolean, - val support158: Boolean, - val enableHideBl: Boolean, - val enableCleanupResidue: Boolean, - val umountForZygoteIsoService: Boolean, - val enableAvcLogSpoofing: Boolean - ) { - /** - * 检查是否有需要自启动的配置 - */ - fun hasAutoStartConfig(): Boolean { - return unameValue != DEFAULT_UNAME || - buildTimeValue != DEFAULT_BUILD_TIME || - susPaths.isNotEmpty() || - susLoopPaths.isNotEmpty() || - susMaps.isNotEmpty() || - susMounts.isNotEmpty() || - tryUmounts.isNotEmpty() || - kstatConfigs.isNotEmpty() || - addKstatPaths.isNotEmpty() - } - } - - // 基础工具方法 - private fun getPrefs(context: Context): SharedPreferences = - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - private fun getSuSFSVersionUse(context: Context): String = try { - val version = getSuSFSVersion() - val binaryName = "${SUSFS_BINARY_TARGET_NAME}_${version.removePrefix("v")}" - if (isBinaryAvailable(context, binaryName)) { - version - } else { - MAX_SUSFS_VERSION - } - } catch (_: Exception) { - MAX_SUSFS_VERSION - } - - fun isBinaryAvailable(context: Context, binaryName: String): Boolean = try { - context.assets.open(binaryName).use { true } - } catch (_: IOException) { false } - - private fun getSuSFSBinaryName(context: Context): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse(context).removePrefix("v")}" - - private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/$SUSFS_BINARY_TARGET_NAME" - - private fun runCmd(shell: Shell, cmd: String): String { - return shell.newJob() - .add(cmd) - .to(mutableListOf(), null) - .exec().out - .joinToString("\n") - } - - private fun runCmdWithResult(cmd: String): CommandResult { - val result = Shell.getShell().newJob().add(cmd).exec() - return CommandResult(result.isSuccess, result.out.joinToString("\n"), result.err.joinToString("\n")) - } - - /** - * 版本比较方法 - */ - private fun compareVersions(version1: String, version2: String): Int { - val v1Parts = version1.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } - val v2Parts = version2.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } - - val maxLength = maxOf(v1Parts.size, v2Parts.size) - - for (i in 0 until maxLength) { - val v1Part = v1Parts.getOrNull(i) ?: 0 - val v2Part = v2Parts.getOrNull(i) ?: 0 - - when { - v1Part > v2Part -> return 1 - v1Part < v2Part -> return -1 - } - } - return 0 - } - - private fun isVersionAtLeast(minVersion: String): Boolean = try { - compareVersions(getSuSFSVersion(), minVersion) >= 0 - } catch (_: Exception) { - true - } - // 检查是否支持设置sdcard路径等功能(1.5.8+) - fun isSusVersion158(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_HIDE_MOUNT) - - // 检查是否支持循环路径和AVC日志欺骗等功能(1.5.9+) - fun isSusVersion159(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_LOOP_PATH) - - // 检查是否支持隐藏内存映射(1.5.12+) - fun isSusVersion1512(): Boolean = isVersionAtLeast(MIN_VERSION_SUS_MAPS) - - /** - * 获取当前模块配置 - */ - private fun getCurrentModuleConfig(context: Context): ModuleConfig { - return ModuleConfig( - targetPath = getSuSFSTargetPath(), - unameValue = getUnameValue(context), - buildTimeValue = getBuildTimeValue(context), - executeInPostFsData = getExecuteInPostFsData(context), - susPaths = getSusPaths(context), - susLoopPaths = getSusLoopPaths(context), - susMaps = getSusMaps(context), - susMounts = getSusMounts(context), - tryUmounts = getTryUmounts(context), - androidDataPath = getAndroidDataPath(context), - sdcardPath = getSdcardPath(context), - enableLog = getEnableLogState(context), - kstatConfigs = getKstatConfigs(context), - addKstatPaths = getAddKstatPaths(context), - hideSusMountsForAllProcs = getHideSusMountsForAllProcs(context), - support158 = isSusVersion158(), - enableHideBl = getEnableHideBl(context), - enableCleanupResidue = getEnableCleanupResidue(context), - umountForZygoteIsoService = getUmountForZygoteIsoService(context), - enableAvcLogSpoofing = getEnableAvcLogSpoofing(context) - ) - } - - // 配置存取方法 - fun saveUnameValue(context: Context, value: String) = - getPrefs(context).edit { putString(KEY_UNAME_VALUE, value) } - - fun getUnameValue(context: Context): String = - getPrefs(context).getString(KEY_UNAME_VALUE, DEFAULT_UNAME) ?: DEFAULT_UNAME - - fun saveBuildTimeValue(context: Context, value: String) = - getPrefs(context).edit { putString(KEY_BUILD_TIME_VALUE, value)} - - fun getBuildTimeValue(context: Context): String = - getPrefs(context).getString(KEY_BUILD_TIME_VALUE, DEFAULT_BUILD_TIME) ?: DEFAULT_BUILD_TIME - - fun setAutoStartEnabled(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_AUTO_START_ENABLED, enabled) } - - fun isAutoStartEnabled(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false) - - fun saveEnableLogState(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_ENABLE_LOG, enabled) } - - fun getEnableLogState(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_ENABLE_LOG, false) - - fun getExecuteInPostFsData(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_EXECUTE_IN_POST_FS_DATA, false) - - fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) { - getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) } - if (isAutoStartEnabled(context)) { - CoroutineScope(Dispatchers.Default).launch { - updateMagiskModule(context) - } - } - } - - // SUS挂载隐藏控制 - fun saveHideSusMountsForAllProcs(context: Context, hideForAll: Boolean) = - getPrefs(context).edit { putBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, hideForAll) } - - fun getHideSusMountsForAllProcs(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, true) - - // 隐藏BL锁脚本 - fun saveEnableHideBl(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_ENABLE_HIDE_BL, enabled) } - - fun getEnableHideBl(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_ENABLE_HIDE_BL, true) - - - // 清理残留配置 - fun saveEnableCleanupResidue(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_ENABLE_CLEANUP_RESIDUE, enabled) } - - fun getEnableCleanupResidue(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_ENABLE_CLEANUP_RESIDUE, false) - - // Zygote隔离服务卸载控制 - fun saveUmountForZygoteIsoService(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE, enabled) } - - fun getUmountForZygoteIsoService(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE, false) - - // AVC日志欺骗配置 - fun saveEnableAvcLogSpoofing(context: Context, enabled: Boolean) = - getPrefs(context).edit { putBoolean(KEY_ENABLE_AVC_LOG_SPOOFING, enabled) } - - fun getEnableAvcLogSpoofing(context: Context): Boolean = - getPrefs(context).getBoolean(KEY_ENABLE_AVC_LOG_SPOOFING, false) - - - // 路径和配置管理 - fun saveSusPaths(context: Context, paths: Set) = - getPrefs(context).edit { putStringSet(KEY_SUS_PATHS, paths) } - - fun getSusPaths(context: Context): Set = - getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet() - - // 循环路径管理 - fun saveSusLoopPaths(context: Context, paths: Set) = - getPrefs(context).edit { putStringSet(KEY_SUS_LOOP_PATHS, paths) } - - fun getSusLoopPaths(context: Context): Set = - getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() - - fun saveSusMaps(context: Context, maps: Set) = - getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) } - - fun getSusMaps(context: Context): Set = - getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet() - - fun saveSusMounts(context: Context, mounts: Set) = - getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } - - fun getSusMounts(context: Context): Set = - getPrefs(context).getStringSet(KEY_SUS_MOUNTS, emptySet()) ?: emptySet() - - fun saveTryUmounts(context: Context, umounts: Set) = - getPrefs(context).edit { putStringSet(KEY_TRY_UMOUNTS, umounts) } - - fun getTryUmounts(context: Context): Set = - getPrefs(context).getStringSet(KEY_TRY_UMOUNTS, emptySet()) ?: emptySet() - - fun saveKstatConfigs(context: Context, configs: Set) = - getPrefs(context).edit { putStringSet(KEY_KSTAT_CONFIGS, configs) } - - fun getKstatConfigs(context: Context): Set = - getPrefs(context).getStringSet(KEY_KSTAT_CONFIGS, emptySet()) ?: emptySet() - - fun saveAddKstatPaths(context: Context, paths: Set) = - getPrefs(context).edit { putStringSet(KEY_ADD_KSTAT_PATHS, paths) } - - fun getAddKstatPaths(context: Context): Set = - getPrefs(context).getStringSet(KEY_ADD_KSTAT_PATHS, emptySet()) ?: emptySet() - - @SuppressLint("SdCardPath") - fun saveAndroidDataPath(context: Context, path: String) = - getPrefs(context).edit { putString(KEY_ANDROID_DATA_PATH, path) } - - @SuppressLint("SdCardPath") - fun getAndroidDataPath(context: Context): String = - getPrefs(context).getString(KEY_ANDROID_DATA_PATH, "/sdcard/Android/data") ?: "/sdcard/Android/data" - - @SuppressLint("SdCardPath") - fun saveSdcardPath(context: Context, path: String) = - getPrefs(context).edit { putString(KEY_SDCARD_PATH, path) } - - @SuppressLint("SdCardPath") - fun getSdcardPath(context: Context): String = - getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard" - - // 获取已安装的应用列表 - @SuppressLint("QueryPermissionsNeeded") - suspend fun getInstalledApps(): List = withContext(Dispatchers.IO) { - try { - val allApps = mutableMapOf() - - // 从SuperUser中获取应用 - SuperUserViewModel.apps.forEach { superUserApp -> - try { - val isSystemApp = superUserApp.packageInfo.applicationInfo?.let { - (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 - } ?: false - if (!isSystemApp) { - allApps[superUserApp.packageName] = AppInfo( - packageName = superUserApp.packageName, - appName = superUserApp.label, - packageInfo = superUserApp.packageInfo, - isSystemApp = false - ) - } - } catch (_: Exception) { - } - } - - // 检查每个应用的数据目录是否存在 - val filteredApps = allApps.values.map { appInfo -> - async(Dispatchers.IO) { - val dataPath = "$MEDIA_DATA_PATH/${appInfo.packageName}" - val exists = try { - val shell = getRootShell() - val outputList = mutableListOf() - val errorList = mutableListOf() - - val result = shell.newJob() - .add("[ -d \"$dataPath\" ] && echo 'exists' || echo 'not_exists'") - .to(outputList, errorList) - .exec() - - result.isSuccess && outputList.isNotEmpty() && outputList[0].trim() == "exists" - } catch (e: Exception) { - Log.w("SuSFSManager", "Failed to check directory for ${appInfo.packageName}: ${e.message}") - false - } - if (exists) appInfo else null - } - }.awaitAll().filterNotNull() - - filteredApps.sortedBy { it.appName } - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - - // 获取应用的UID - private suspend fun getAppUid(context: Context, packageName: String): Int? = withContext(Dispatchers.IO) { - try { - // 从SuperUserViewModel中查找 - val superUserApp = SuperUserViewModel.apps.find { it.packageName == packageName } - if (superUserApp != null) { - return@withContext superUserApp.packageInfo.applicationInfo?.uid - } - - // 从PackageManager中查找 - val packageManager = context.packageManager - val packageInfo = packageManager.getPackageInfo(packageName, 0) - packageInfo.applicationInfo?.uid - } catch (e: Exception) { - Log.w("SuSFSManager", "Failed to get UID for package $packageName: ${e.message}") - null - } - } - - private fun buildUidPath(uid: Int): String = "$CGROUP_UID_PATH_PREFIX$uid" - - - // 快捷添加应用路径 - suspend fun addAppPaths(context: Context, packageName: String): Boolean { - val androidDataPath = getAndroidDataPath(context) - getSdcardPath(context) - - val path1 = "$androidDataPath/$packageName" - val path2 = "$MEDIA_DATA_PATH/$packageName" - - val uid = getAppUid(context, packageName) - if (uid == null) { - Log.w("SuSFSManager", "Failed to get UID for package: $packageName") - return false - } - - val path3 = buildUidPath(uid) - - var successCount = 0 - val totalCount = 3 - - // 添加第一个路径(Android/data路径) - if (addSusPath(context, path1)) { - successCount++ - } - - // 添加第二个路径(媒体数据路径) - if (addSusPath(context, path2)) { - successCount++ - } - - // 添加第三个路径(UID路径) - if (addSusPath(context, path3)) { - successCount++ - } - - val success = successCount > 0 - - Log.d("SuSFSManager", "Added $successCount/$totalCount paths for $packageName (UID: $uid)") - - return success - } - - // 获取所有配置的Map - private fun getAllConfigurations(context: Context): Map { - return mapOf( - KEY_UNAME_VALUE to getUnameValue(context), - KEY_BUILD_TIME_VALUE to getBuildTimeValue(context), - KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), - KEY_SUS_PATHS to getSusPaths(context), - KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), - KEY_SUS_MAPS to getSusMaps(context), - KEY_SUS_MOUNTS to getSusMounts(context), - KEY_TRY_UMOUNTS to getTryUmounts(context), - KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), - KEY_SDCARD_PATH to getSdcardPath(context), - KEY_ENABLE_LOG to getEnableLogState(context), - KEY_EXECUTE_IN_POST_FS_DATA to getExecuteInPostFsData(context), - KEY_KSTAT_CONFIGS to getKstatConfigs(context), - KEY_ADD_KSTAT_PATHS to getAddKstatPaths(context), - KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS to getHideSusMountsForAllProcs(context), - KEY_ENABLE_HIDE_BL to getEnableHideBl(context), - KEY_ENABLE_CLEANUP_RESIDUE to getEnableCleanupResidue(context), - KEY_UMOUNT_FOR_ZYGOTE_ISO_SERVICE to getUmountForZygoteIsoService(context), - KEY_ENABLE_AVC_LOG_SPOOFING to getEnableAvcLogSpoofing(context), - ) - } - - //生成备份文件名 - private fun generateBackupFileName(): String { - val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) - val timestamp = dateFormat.format(Date()) - return "SuSFS_Config_$timestamp$BACKUP_FILE_EXTENSION" - } - - // 获取设备信息 - private fun getDeviceInfo(): String { - return try { - "${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})" - } catch (_: Exception) { - "Unknown Device" - } - } - - // 创建配置备份 - suspend fun createBackup(context: Context, backupFilePath: String): Boolean = withContext(Dispatchers.IO) { - try { - val configurations = getAllConfigurations(context) - val backupData = BackupData( - version = getSuSFSVersion(), - timestamp = System.currentTimeMillis(), - deviceInfo = getDeviceInfo(), - configurations = configurations - ) - - val backupFile = File(backupFilePath) - backupFile.parentFile?.mkdirs() - - backupFile.writeText(backupData.toJson()) - - showToast(context, context.getString(R.string.susfs_backup_success, backupFile.name)) - true - } catch (e: Exception) { - e.printStackTrace() - showToast(context, context.getString(R.string.susfs_backup_failed, e.message ?: "Unknown error")) - false - } - } - - //从备份文件还原配置 - suspend fun restoreFromBackup(context: Context, backupFilePath: String): Boolean = withContext(Dispatchers.IO) { - try { - val backupFile = File(backupFilePath) - if (!backupFile.exists()) { - showToast(context, context.getString(R.string.susfs_backup_file_not_found)) - return@withContext false - } - - val backupContent = backupFile.readText() - val backupData = BackupData.fromJson(backupContent) - - if (backupData == null) { - showToast(context, context.getString(R.string.susfs_backup_invalid_format)) - return@withContext false - } - - // 检查备份版本兼容性 - if (backupData.version != getSuSFSVersion()) { - showToast(context, context.getString(R.string.susfs_backup_version_mismatch)) - } - - // 还原所有配置 - restoreConfigurations(context, backupData.configurations) - - // 如果自启动已启用,更新模块 - if (isAutoStartEnabled(context)) { - updateMagiskModule(context) - } - - val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - val backupDate = dateFormat.format(Date(backupData.timestamp)) - - showToast(context, context.getString(R.string.susfs_restore_success, backupDate, backupData.deviceInfo)) - true - } catch (e: Exception) { - e.printStackTrace() - showToast(context, context.getString(R.string.susfs_restore_failed, e.message ?: "Unknown error")) - false - } - } - - - // 还原配置到SharedPreferences - private fun restoreConfigurations(context: Context, configurations: Map) { - val prefs = getPrefs(context) - prefs.edit { - configurations.forEach { (key, value) -> - when (value) { - is String -> putString(key, value) - is Boolean -> putBoolean(key, value) - is Set<*> -> { - @Suppress("UNCHECKED_CAST") - putStringSet(key, value as Set) - } - is Int -> putInt(key, value) - is Long -> putLong(key, value) - is Float -> putFloat(key, value) - } - } - } - } - - // 验证备份文件 - suspend fun validateBackupFile(backupFilePath: String): BackupData? = withContext(Dispatchers.IO) { - try { - val backupFile = File(backupFilePath) - if (!backupFile.exists()) { - return@withContext null - } - - val backupContent = backupFile.readText() - BackupData.fromJson(backupContent) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - // 获取备份文件路径 - fun getDefaultBackupFileName(): String { - return generateBackupFileName() - } - - // 槽位信息获取 - suspend fun getCurrentSlotInfo(): List = withContext(Dispatchers.IO) { - try { - val slotInfoList = mutableListOf() - val shell = Shell.getShell() - - listOf("boot_a", "boot_b").forEach { slot -> - val unameCmd = - "strings -n 20 /dev/block/by-name/$slot | awk '/Linux version/ && ++c==2 {print $3; exit}'" - val buildTimeCmd = "strings -n 20 /dev/block/by-name/$slot | sed -n '/Linux version.*#/{s/.*#/#/p;q}'" - - val uname = runCmd(shell, unameCmd).trim() - val buildTime = runCmd(shell, buildTimeCmd).trim() - - if (uname.isNotEmpty() && buildTime.isNotEmpty()) { - slotInfoList.add(SlotInfo(slot, uname.ifEmpty { "unknown" }, buildTime.ifEmpty { "unknown" })) - } - } - - slotInfoList - } catch (e: Exception) { - e.printStackTrace() - emptyList() - } - } - - suspend fun getCurrentActiveSlot(): String = withContext(Dispatchers.IO) { - try { - val shell = Shell.getShell() - val suffix = runCmd(shell, "getprop ro.boot.slot_suffix").trim() - when (suffix) { - "_a" -> "boot_a" - "_b" -> "boot_b" - else -> "unknown" - } - } catch (_: Exception) { - "unknown" - } - } - - // 二进制文件管理 - private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) { - try { - val binaryName = getSuSFSBinaryName(context) - val targetPath = getSuSFSTargetPath() - val tempFile = File(context.cacheDir, binaryName) - - context.assets.open(binaryName).use { input -> - FileOutputStream(tempFile).use { output -> - input.copyTo(output) - } - } - - val success = runCmdWithResult("cp '${tempFile.absolutePath}' '$targetPath' && chmod 755 '$targetPath'").isSuccess - tempFile.delete() - - if (success && runCmdWithResult("test -f '$targetPath'").isSuccess) targetPath else null - } catch (e: IOException) { - e.printStackTrace() - null - } - } - - fun isBinaryAvailable(context: Context): Boolean = try { - context.assets.open(getSuSFSBinaryName(context)).use { true } - } catch (_: IOException) { false } - - // 命令执行 - private suspend fun executeSusfsCommand(context: Context, command: String): Boolean = withContext(Dispatchers.IO) { - try { - val binaryPath = copyBinaryFromAssets(context) ?: run { - showToast(context, context.getString(R.string.susfs_binary_not_found)) - return@withContext false - } - - val result = runCmdWithResult("$binaryPath $command") - - if (!result.isSuccess) { - showToast(context, "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}") - } - - result.isSuccess - } catch (e: Exception) { - e.printStackTrace() - showToast(context, context.getString(R.string.susfs_command_error, e.message ?: "Unknown error")) - false - } - } - - private suspend fun executeSusfsCommandWithOutput(context: Context, command: String): CommandResult = withContext(Dispatchers.IO) { - try { - val binaryPath = copyBinaryFromAssets(context) ?: return@withContext CommandResult( - false, "", context.getString(R.string.susfs_binary_not_found) - ) - runCmdWithResult("$binaryPath $command") - } catch (e: Exception) { - e.printStackTrace() - CommandResult(false, "", e.message ?: "Unknown error") - } - } - - private suspend fun showToast(context: Context, message: String) = withContext(Dispatchers.Main) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } - - /** - * 模块管理 - */ - private suspend fun updateMagiskModule(context: Context): Boolean { - return removeMagiskModule() && createMagiskModule(context) - } - - /** - * 模块创建方法 - */ - private suspend fun createMagiskModule(context: Context): Boolean = withContext(Dispatchers.IO) { - try { - val config = getCurrentModuleConfig(context) - - // 创建模块目录 - if (!runCmdWithResult("mkdir -p $MODULE_PATH").isSuccess) return@withContext false - - // 创建module.prop - val moduleProp = ScriptGenerator.generateModuleProp(MODULE_ID) - if (!runCmdWithResult("cat > $MODULE_PATH/module.prop << 'EOF'\n$moduleProp\nEOF").isSuccess) return@withContext false - - // 生成并创建所有脚本文件 - val scripts = ScriptGenerator.generateAllScripts(config) - - scripts.all { (filename, content) -> - runCmdWithResult("cat > $MODULE_PATH/$filename << 'EOF'\n$content\nEOF").isSuccess && - runCmdWithResult("chmod 755 $MODULE_PATH/$filename").isSuccess - } - } catch (e: Exception) { - e.printStackTrace() - false - } - } - - private suspend fun removeMagiskModule(): Boolean = withContext(Dispatchers.IO) { - try { - runCmdWithResult("rm -rf $MODULE_PATH").isSuccess - } catch (e: Exception) { - e.printStackTrace() - false - } - } - - // 功能状态获取 - suspend fun getEnabledFeatures(context: Context): List = withContext(Dispatchers.IO) { - try { - val featuresOutput = getSuSFSFeatures() - - if (featuresOutput.isNotBlank() && featuresOutput != "Invalid") { - parseEnabledFeaturesFromOutput(context, featuresOutput) - } else { - getDefaultDisabledFeatures(context) - } - } catch (e: Exception) { - e.printStackTrace() - getDefaultDisabledFeatures(context) - } - } - - private fun parseEnabledFeaturesFromOutput(context: Context, featuresOutput: String): List { - val enabledConfigs = featuresOutput.lines() - .map { it.trim() } - .filter { it.isNotEmpty() } - .toSet() - - val featureMap = mapOf( - "CONFIG_KSU_SUSFS_SUS_PATH" to context.getString(R.string.sus_path_feature_label), - "CONFIG_KSU_SUSFS_SUS_MOUNT" to context.getString(R.string.sus_mount_feature_label), - "CONFIG_KSU_SUSFS_TRY_UMOUNT" to context.getString(R.string.try_umount_feature_label), - "CONFIG_KSU_SUSFS_SPOOF_UNAME" to context.getString(R.string.spoof_uname_feature_label), - "CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label), - "CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label), - "CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label), - "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_KSU_DEFAULT_MOUNT" to context.getString(R.string.auto_default_mount_feature_label), - "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_BIND_MOUNT" to context.getString(R.string.auto_bind_mount_feature_label), - "CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label), - "CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label), - "CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label), - "CONFIG_KSU_SUSFS_SUS_SU" to context.getString(R.string.sus_su_feature_label) - ) - - - return featureMap.map { (configKey, displayName) -> - val isEnabled = enabledConfigs.contains(configKey) - - val statusText = if (isEnabled) { - context.getString(R.string.susfs_feature_enabled) - } else { - context.getString(R.string.susfs_feature_disabled) - } - - val canConfigure = displayName == context.getString(R.string.enable_log_feature_label) - - EnabledFeature(displayName, isEnabled, statusText, canConfigure) - }.sortedBy { it.name } - } - - private fun getDefaultDisabledFeatures(context: Context): List { - val defaultFeatures = listOf( - "sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), - "sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label), - "try_umount_feature_label" to context.getString(R.string.try_umount_feature_label), - "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label), - "spoof_cmdline_feature_label" to context.getString(R.string.spoof_cmdline_feature_label), - "open_redirect_feature_label" to context.getString(R.string.open_redirect_feature_label), - "enable_log_feature_label" to context.getString(R.string.enable_log_feature_label), - "auto_default_mount_feature_label" to context.getString(R.string.auto_default_mount_feature_label), - "auto_bind_mount_feature_label" to context.getString(R.string.auto_bind_mount_feature_label), - "auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label), - "hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label), - "sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label), - "sus_su_feature_label" to context.getString(R.string.sus_su_feature_label) - ) - - return defaultFeatures.map { (_, displayName) -> - EnabledFeature( - name = displayName, - isEnabled = false, - statusText = context.getString(R.string.susfs_feature_disabled), - canConfigure = displayName == context.getString(R.string.enable_log_feature_label) - ) - }.sortedBy { it.name } - } - - // sus日志开关 - suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean { - val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}") - if (success) { - saveEnableLogState(context, enabled) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, if (enabled) context.getString(R.string.susfs_log_enabled) else context.getString(R.string.susfs_log_disabled)) - } - return success - } - - // AVC日志欺骗开关 - suspend fun setEnableAvcLogSpoofing(context: Context, enabled: Boolean): Boolean { - if (!isSusVersion159()) { - return false - } - - val success = executeSusfsCommand(context, "enable_avc_log_spoofing ${if (enabled) 1 else 0}") - if (success) { - saveEnableAvcLogSpoofing(context, enabled) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, if (enabled) - context.getString(R.string.avc_log_spoofing_enabled) - else - context.getString(R.string.avc_log_spoofing_disabled) - ) - } - return success - } - - // SUS挂载隐藏控制 - suspend fun setHideSusMountsForAllProcs(context: Context, hideForAll: Boolean): Boolean { - if (!isSusVersion158()) { - return false - } - - val success = executeSusfsCommand(context, "hide_sus_mnts_for_all_procs ${if (hideForAll) 1 else 0}") - if (success) { - saveHideSusMountsForAllProcs(context, hideForAll) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, if (hideForAll) - context.getString(R.string.susfs_hide_mounts_all_enabled) - else - context.getString(R.string.susfs_hide_mounts_all_disabled) - ) - } - return success - } - - // uname和构建时间 - @SuppressLint("StringFormatMatches") - suspend fun setUname(context: Context, unameValue: String, buildTimeValue: String): Boolean { - val success = executeSusfsCommand(context, "set_uname '$unameValue' '$buildTimeValue'") - if (success) { - saveUnameValue(context, unameValue) - saveBuildTimeValue(context, buildTimeValue) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_uname_set_success, unameValue, buildTimeValue)) - } - return success - } - - // 添加SUS路径 - @SuppressLint("StringFormatInvalid") - suspend fun addSusPath(context: Context, path: String): Boolean { - // 如果是1.5.8版本,先设置路径配置 - if (isSusVersion158()) { - // 获取当前配置的路径,如果没有配置则使用默认值 - val androidDataPath = getAndroidDataPath(context) - val sdcardPath = getSdcardPath(context) - - // 先设置Android Data路径 - val androidDataSuccess = executeSusfsCommand(context, "set_android_data_root_path '$androidDataPath'") - if (androidDataSuccess) { - showToast(context, context.getString(R.string.susfs_android_data_path_set, androidDataPath)) - } - - // 再设置SD卡路径 - val sdcardSuccess = executeSusfsCommand(context, "set_sdcard_root_path '$sdcardPath'") - if (sdcardSuccess) { - showToast(context, context.getString(R.string.susfs_sdcard_path_set, sdcardPath)) - } - - // 如果路径设置失败,记录但不阻止继续执行 - if (!androidDataSuccess || !sdcardSuccess) { - showToast(context, context.getString(R.string.susfs_path_setup_warning)) - } - } - - // 执行添加SUS路径命令 - val result = executeSusfsCommandWithOutput(context, "add_sus_path '$path'") - val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding") - - if (isActuallySuccessful) { - saveSusPaths(context, getSusPaths(context) + path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_sus_path_added_success, path)) - } else { - val errorMessage = if (result.output.contains("not found, skip adding")) { - context.getString(R.string.susfs_path_not_found_error, path) - } else { - "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}" - } - showToast(context, errorMessage) - } - return isActuallySuccessful - } - - suspend fun removeSusPath(context: Context, path: String): Boolean { - saveSusPaths(context, getSusPaths(context) - path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "SUS path removed: $path") - return true - } - - // 编辑SUS路径 - suspend fun editSusPath(context: Context, oldPath: String, newPath: String): Boolean { - return try { - val currentPaths = getSusPaths(context).toMutableSet() - if (!currentPaths.remove(oldPath)) { - showToast(context, "Original path not found: $oldPath") - return false - } - - saveSusPaths(context, currentPaths) - - val success = addSusPath(context, newPath) - - if (success) { - showToast(context, "SUS path updated: $oldPath -> $newPath") - return true - } else { - // 如果添加新路径失败,恢复旧路径 - currentPaths.add(oldPath) - saveSusPaths(context, currentPaths) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update path, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating SUS path: ${e.message}") - false - } - } - - // 循环路径相关方法 - @SuppressLint("SdCardPath") - private fun isValidLoopPath(path: String): Boolean { - return !path.startsWith("/storage/") && !path.startsWith("/sdcard/") - } - - @SuppressLint("StringFormatInvalid") - suspend fun addSusLoopPath(context: Context, path: String): Boolean { - // 检查路径是否有效 - if (!isValidLoopPath(path)) { - showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) - return false - } - - // 执行添加循环路径命令 - val result = executeSusfsCommandWithOutput(context, "add_sus_path_loop '$path'") - val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding") - - if (isActuallySuccessful) { - saveSusLoopPaths(context, getSusLoopPaths(context) + path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_loop_path_added_success, path)) - } else { - val errorMessage = if (result.output.contains("not found, skip adding")) { - context.getString(R.string.susfs_path_not_found_error, path) - } else { - "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}" - } - showToast(context, errorMessage) - } - return isActuallySuccessful - } - - suspend fun removeSusLoopPath(context: Context, path: String): Boolean { - saveSusLoopPaths(context, getSusLoopPaths(context) - path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_loop_path_removed, path)) - return true - } - - // 编辑循环路径 - suspend fun editSusLoopPath(context: Context, oldPath: String, newPath: String): Boolean { - // 检查新路径是否有效 - if (!isValidLoopPath(newPath)) { - showToast(context, context.getString(R.string.susfs_loop_path_invalid_location)) - return false - } - - return try { - val currentPaths = getSusLoopPaths(context).toMutableSet() - if (!currentPaths.remove(oldPath)) { - showToast(context, "Original loop path not found: $oldPath") - return false - } - - saveSusLoopPaths(context, currentPaths) - - val success = addSusLoopPath(context, newPath) - - if (success) { - showToast(context, context.getString(R.string.susfs_loop_path_updated, oldPath, newPath)) - return true - } else { - // 如果添加新路径失败,恢复旧路径 - currentPaths.add(oldPath) - saveSusLoopPaths(context, currentPaths) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update loop path, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating SUS loop path: ${e.message}") - false - } - } - - // 添加 SUS Maps - suspend fun addSusMap(context: Context, map: String): Boolean { - val success = executeSusfsCommand(context, "add_sus_map '$map'") - if (success) { - saveSusMaps(context, getSusMaps(context) + map) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_sus_map_added_success, map)) - } - return success - } - - suspend fun removeSusMap(context: Context, map: String): Boolean { - saveSusMaps(context, getSusMaps(context) - map) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.susfs_sus_map_removed, map)) - return true - } - - suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean { - return try { - val currentMaps = getSusMaps(context).toMutableSet() - if (!currentMaps.remove(oldMap)) { - showToast(context, "Original SUS map not found: $oldMap") - return false - } - - saveSusMaps(context, currentMaps) - - val success = addSusMap(context, newMap) - - if (success) { - showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap)) - return true - } else { - // 如果添加新映射失败,恢复旧映射 - currentMaps.add(oldMap) - saveSusMaps(context, currentMaps) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update SUS map, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating SUS map: ${e.message}") - false - } - } - - // 添加SUS挂载 - suspend fun addSusMount(context: Context, mount: String): Boolean { - val success = executeSusfsCommand(context, "add_sus_mount '$mount'") - if (success) { - saveSusMounts(context, getSusMounts(context) + mount) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - } - return success - } - - suspend fun removeSusMount(context: Context, mount: String): Boolean { - saveSusMounts(context, getSusMounts(context) - mount) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Removed SUS mount: $mount") - return true - } - - // 编辑SUS挂载 - suspend fun editSusMount(context: Context, oldMount: String, newMount: String): Boolean { - return try { - val currentMounts = getSusMounts(context).toMutableSet() - if (!currentMounts.remove(oldMount)) { - showToast(context, "Original mount not found: $oldMount") - return false - } - - saveSusMounts(context, currentMounts) - - val success = addSusMount(context, newMount) - - if (success) { - showToast(context, "SUS mount updated: $oldMount -> $newMount") - return true - } else { - // 如果添加新挂载点失败,恢复旧挂载点 - currentMounts.add(oldMount) - saveSusMounts(context, currentMounts) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update mount, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating SUS mount: ${e.message}") - false - } - } - - // 添加尝试卸载 - suspend fun addTryUmount(context: Context, path: String, mode: Int): Boolean { - val commandSuccess = executeSusfsCommand(context, "add_try_umount '$path' $mode") - saveTryUmounts(context, getTryUmounts(context) + "$path|$mode") - if (isAutoStartEnabled(context)) updateMagiskModule(context) - - showToast(context, if (commandSuccess) { - context.getString(R.string.susfs_try_umount_added_success, path) - } else { - context.getString(R.string.susfs_try_umount_added_saved, path) - }) - return true - } - - suspend fun removeTryUmount(context: Context, umountEntry: String): Boolean { - saveTryUmounts(context, getTryUmounts(context) - umountEntry) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - val path = umountEntry.split("|").firstOrNull() ?: umountEntry - showToast(context, "Removed Try to uninstall: $path") - return true - } - - // 编辑尝试卸载 - suspend fun editTryUmount(context: Context, oldEntry: String, newPath: String, newMode: Int): Boolean { - return try { - val currentUmounts = getTryUmounts(context).toMutableSet() - if (!currentUmounts.remove(oldEntry)) { - showToast(context, "Original umount entry not found: $oldEntry") - return false - } - - saveTryUmounts(context, currentUmounts) - - val success = addTryUmount(context, newPath, newMode) - - if (success) { - showToast(context, "Try umount updated: $oldEntry -> $newPath|$newMode") - return true - } else { - // 如果添加新条目失败,恢复旧条目 - currentUmounts.add(oldEntry) - saveTryUmounts(context, currentUmounts) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update umount entry, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating try umount: ${e.message}") - false - } - } - - // Zygote隔离服务卸载控制 - suspend fun setUmountForZygoteIsoService(context: Context, enabled: Boolean): Boolean { - if (!isSusVersion158()) { - return false - } - - val result = executeSusfsCommandWithOutput(context, "umount_for_zygote_iso_service ${if (enabled) 1 else 0}") - val success = result.isSuccess && result.output.isEmpty() - - if (success) { - saveUmountForZygoteIsoService(context, enabled) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, if (enabled) - context.getString(R.string.umount_zygote_iso_service_enabled) - else - context.getString(R.string.umount_zygote_iso_service_disabled) - ) - } else { - showToast(context, context.getString(R.string.susfs_command_failed)) - } - return success - } - - // 添加kstat配置 - suspend fun addKstatStatically(context: Context, path: String, ino: String, dev: String, nlink: String, - size: String, atime: String, atimeNsec: String, mtime: String, mtimeNsec: String, - ctime: String, ctimeNsec: String, blocks: String, blksize: String): Boolean { - val command = "add_sus_kstat_statically '$path' '$ino' '$dev' '$nlink' '$size' '$atime' '$atimeNsec' '$mtime' '$mtimeNsec' '$ctime' '$ctimeNsec' '$blocks' '$blksize'" - val success = executeSusfsCommand(context, command) - if (success) { - val configEntry = "$path|$ino|$dev|$nlink|$size|$atime|$atimeNsec|$mtime|$mtimeNsec|$ctime|$ctimeNsec|$blocks|$blksize" - saveKstatConfigs(context, getKstatConfigs(context) + configEntry) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.kstat_static_config_added, path)) - } - return success - } - - suspend fun removeKstatConfig(context: Context, config: String): Boolean { - saveKstatConfigs(context, getKstatConfigs(context) - config) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - val path = config.split("|").firstOrNull() ?: config - showToast(context, context.getString(R.string.kstat_config_removed, path)) - return true - } - - // 编辑kstat配置 - @SuppressLint("StringFormatInvalid") - suspend fun editKstatConfig(context: Context, oldConfig: String, path: String, ino: String, dev: String, nlink: String, - size: String, atime: String, atimeNsec: String, mtime: String, mtimeNsec: String, - ctime: String, ctimeNsec: String, blocks: String, blksize: String): Boolean { - return try { - val currentConfigs = getKstatConfigs(context).toMutableSet() - if (!currentConfigs.remove(oldConfig)) { - showToast(context, "Original kstat config not found") - return false - } - - saveKstatConfigs(context, currentConfigs) - - val success = addKstatStatically(context, path, ino, dev, nlink, size, atime, atimeNsec, - mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize) - - if (success) { - showToast(context, context.getString(R.string.kstat_config_updated, path)) - return true - } else { - // 如果添加新配置失败,恢复旧配置 - currentConfigs.add(oldConfig) - saveKstatConfigs(context, currentConfigs) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update kstat config, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating kstat config: ${e.message}") - false - } - } - - // 添加kstat路径 - suspend fun addKstat(context: Context, path: String): Boolean { - val success = executeSusfsCommand(context, "add_sus_kstat '$path'") - if (success) { - saveAddKstatPaths(context, getAddKstatPaths(context) + path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.kstat_path_added, path)) - } - return success - } - - suspend fun removeAddKstat(context: Context, path: String): Boolean { - saveAddKstatPaths(context, getAddKstatPaths(context) - path) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, context.getString(R.string.kstat_path_removed, path)) - return true - } - - // 编辑kstat路径 - @SuppressLint("StringFormatInvalid") - suspend fun editAddKstat(context: Context, oldPath: String, newPath: String): Boolean { - return try { - val currentPaths = getAddKstatPaths(context).toMutableSet() - if (!currentPaths.remove(oldPath)) { - showToast(context, "Original kstat path not found: $oldPath") - return false - } - - saveAddKstatPaths(context, currentPaths) - - val success = addKstat(context, newPath) - - if (success) { - showToast(context, context.getString(R.string.kstat_path_updated, oldPath, newPath)) - return true - } else { - // 如果添加新路径失败,恢复旧路径 - currentPaths.add(oldPath) - saveAddKstatPaths(context, currentPaths) - if (isAutoStartEnabled(context)) updateMagiskModule(context) - showToast(context, "Failed to update kstat path, reverted to original") - return false - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, "Error updating kstat path: ${e.message}") - false - } - } - - // 更新kstat - suspend fun updateKstat(context: Context, path: String): Boolean { - val success = executeSusfsCommand(context, "update_sus_kstat '$path'") - if (success) showToast(context, context.getString(R.string.kstat_updated, path)) - return success - } - - // 更新kstat全克隆 - suspend fun updateKstatFullClone(context: Context, path: String): Boolean { - val success = executeSusfsCommand(context, "update_sus_kstat_full_clone '$path'") - if (success) showToast(context, context.getString(R.string.kstat_full_clone_updated, path)) - return success - } - - // 设置Android数据路径 - suspend fun setAndroidDataPath(context: Context, path: String): Boolean { - val success = executeSusfsCommand(context, "set_android_data_root_path '$path'") - if (success) { - saveAndroidDataPath(context, path) - if (isAutoStartEnabled(context)) { - CoroutineScope(Dispatchers.Default).launch { - updateMagiskModule(context) - } - } - } - return success - } - - // 设置SD卡路径 - suspend fun setSdcardPath(context: Context, path: String): Boolean { - val success = executeSusfsCommand(context, "set_sdcard_root_path '$path'") - if (success) { - saveSdcardPath(context, path) - if (isAutoStartEnabled(context)) { - CoroutineScope(Dispatchers.Default).launch { - updateMagiskModule(context) - } - } - } - return success - } - - /** - * 自启动配置检查 - */ - fun hasConfigurationForAutoStart(context: Context): Boolean { - val config = getCurrentModuleConfig(context) - return config.hasAutoStartConfig() || runBlocking { - getEnabledFeatures(context).any { it.isEnabled } - } - } - - /** - * 自启动配置方法 - */ - suspend fun configureAutoStart(context: Context, enabled: Boolean): Boolean = withContext(Dispatchers.IO) { - try { - if (enabled) { - if (!hasConfigurationForAutoStart(context)) { - showToast(context, context.getString(R.string.susfs_no_config_to_autostart)) - return@withContext false - } - - val targetPath = getSuSFSTargetPath() - if (!runCmdWithResult("test -f '$targetPath'").isSuccess) { - copyBinaryFromAssets(context) ?: run { - showToast(context, context.getString(R.string.susfs_binary_not_found)) - return@withContext false - } - } - - val success = createMagiskModule(context) - if (success) { - setAutoStartEnabled(context, true) - showToast(context, context.getString(R.string.susfs_autostart_enabled_success, MODULE_PATH)) - } else { - showToast(context, context.getString(R.string.susfs_autostart_enable_failed)) - } - success - } else { - val success = removeMagiskModule() - if (success) { - setAutoStartEnabled(context, false) - showToast(context, context.getString(R.string.susfs_autostart_disabled_success)) - } else { - showToast(context, context.getString(R.string.susfs_autostart_disable_failed)) - } - success - } - } catch (e: Exception) { - e.printStackTrace() - showToast(context, context.getString(R.string.susfs_autostart_error, e.message ?: "Unknown error")) - false - } - } - - suspend fun resetToDefault(context: Context): Boolean { - val success = setUname(context, DEFAULT_UNAME, DEFAULT_BUILD_TIME) - if (success && isAutoStartEnabled(context)) { - configureAutoStart(context, false) - } - return success - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt deleted file mode 100644 index e0d9baef..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt +++ /dev/null @@ -1,555 +0,0 @@ -package com.sukisu.ultra.ui.susfs.util - -import android.annotation.SuppressLint - -/** - * Magisk模块脚本生成器 - * 用于生成各种启动脚本的内容 - */ -object ScriptGenerator { - - // 常量定义 - private const val DEFAULT_UNAME = "default" - private const val DEFAULT_BUILD_TIME = "default" - private const val LOG_DIR = "/data/adb/ksu/log" - - /** - * 生成所有脚本文件 - */ - fun generateAllScripts(config: SuSFSManager.ModuleConfig): Map { - return mapOf( - "service.sh" to generateServiceScript(config), - "post-fs-data.sh" to generatePostFsDataScript(config), - "post-mount.sh" to generatePostMountScript(config), - "boot-completed.sh" to generateBootCompletedScript(config) - ) - } - - // 日志相关的通用脚本片段 - private fun generateLogSetup(logFileName: String): String = """ - # 日志目录 - LOG_DIR="$LOG_DIR" - LOG_FILE="${'$'}LOG_DIR/$logFileName" - - # 创建日志目录 - mkdir -p "${'$'}LOG_DIR" - - # 获取当前时间 - get_current_time() { - date '+%Y-%m-%d %H:%M:%S' - } - """.trimIndent() - - // 二进制文件检查的通用脚本片段 - private fun generateBinaryCheck(targetPath: String): String = """ - # 检查SuSFS二进制文件 - SUSFS_BIN="$targetPath" - if [ ! -f "${'$'}SUSFS_BIN" ]; then - echo "$(get_current_time): SuSFS二进制文件未找到: ${'$'}SUSFS_BIN" >> "${'$'}LOG_FILE" - exit 1 - fi - """.trimIndent() - - /** - * 生成service.sh脚本内容 - */ - @SuppressLint("SdCardPath") - private fun generateServiceScript(config: SuSFSManager.ModuleConfig): String { - return buildString { - appendLine("#!/system/bin/sh") - appendLine("# SuSFS Service Script") - appendLine("# 在系统服务启动后执行") - appendLine() - appendLine(generateLogSetup("susfs_service.log")) - appendLine() - appendLine(generateBinaryCheck(config.targetPath)) - appendLine() - - if (shouldConfigureInService(config)) { - // 添加SUS路径 (仅在不支持隐藏挂载时) - if (!config.support158 && config.susPaths.isNotEmpty()) { - appendLine() - appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") - appendLine("sleep 45") - generateSusPathsSection(config.susPaths) - } - - // 设置uname和构建时间 - generateUnameSection(config) - - // 添加Kstat配置 - generateKstatSection(config.kstatConfigs, config.addKstatPaths) - } - - // 添加日志设置 - generateLogSettingSection(config.enableLog) - - // 隐藏BL相关配置 - if (config.enableHideBl) { - generateHideBlSection() - } - - // 清理工具残留 - if (config.enableCleanupResidue) { - generateCleanupResidueSection() - } - - appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"${'$'}LOG_FILE\"") - } - } - - /** - * 判断是否需要在service中配置 - */ - private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean { - return config.susPaths.isNotEmpty() || - config.susLoopPaths.isNotEmpty() || - config.kstatConfigs.isNotEmpty() || - config.addKstatPaths.isNotEmpty() || - (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) - } - - private fun StringBuilder.generateLogSettingSection(enableLog: Boolean) { - appendLine("# 设置日志启用状态") - val logValue = if (enableLog) 1 else 0 - appendLine("\"${'$'}SUSFS_BIN\" enable_log $logValue") - appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - - private fun StringBuilder.generateAvcLogSpoofingSection(enableAvcLogSpoofing: Boolean) { - appendLine("# 设置AVC日志欺骗状态") - val avcLogValue = if (enableAvcLogSpoofing) 1 else 0 - appendLine("\"${'$'}SUSFS_BIN\" enable_avc_log_spoofing $avcLogValue") - appendLine("echo \"$(get_current_time): AVC日志欺骗功能设置为: ${if (enableAvcLogSpoofing) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - - private fun StringBuilder.generateSusPathsSection(susPaths: Set) { - if (susPaths.isNotEmpty()) { - appendLine("# 添加SUS路径") - susPaths.forEach { path -> - appendLine("\"${'$'}SUSFS_BIN\" add_sus_path '$path'") - appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"${'$'}LOG_FILE\"") - } - appendLine() - } - } - - private fun StringBuilder.generateSusLoopPathsSection(susLoopPaths: Set) { - if (susLoopPaths.isNotEmpty()) { - appendLine("# 添加SUS循环路径") - susLoopPaths.forEach { path -> - appendLine("\"${'$'}SUSFS_BIN\" add_sus_path_loop '$path'") - appendLine("echo \"$(get_current_time): 添加SUS循环路径: $path\" >> \"${'$'}LOG_FILE\"") - } - appendLine() - } - } - - @SuppressLint("SdCardPath") - private fun StringBuilder.generateKstatSection( - kstatConfigs: Set, - addKstatPaths: Set - ) { - // 添加Kstat路径 - if (addKstatPaths.isNotEmpty()) { - appendLine("# 添加Kstat路径") - addKstatPaths.forEach { path -> - appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat '$path'") - appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"${'$'}LOG_FILE\"") - } - appendLine() - } - - // 添加Kstat静态配置 - if (kstatConfigs.isNotEmpty()) { - appendLine("# 添加Kstat静态配置") - kstatConfigs.forEach { config -> - val parts = config.split("|") - if (parts.size >= 13) { - val path = parts[0] - val params = parts.drop(1).joinToString("' '", "'", "'") - appendLine() - appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat_statically '$path' $params") - appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"${'$'}LOG_FILE\"") - appendLine() - appendLine("\"${'$'}SUSFS_BIN\" update_sus_kstat '$path'") - appendLine("echo \"$(get_current_time): 更新Kstat配置: $path\" >> \"${'$'}LOG_FILE\"") - } - } - appendLine() - } - } - - private fun StringBuilder.generateUnameSection(config: SuSFSManager.ModuleConfig) { - if (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) { - appendLine("# 设置uname和构建时间") - appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'") - appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - } - - private fun StringBuilder.generateHideBlSection() { - appendLine("# 隐藏BL 来自 Shamiko 脚本") - appendLine( - """ - RESETPROP_BIN="/data/adb/ksu/bin/resetprop" - - check_reset_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) - [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED - } - - check_missing_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) - [ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED - } - - check_missing_match_prop() { - local NAME=$1 - local EXPECTED=$2 - local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME) - [ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED - [ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED - } - - contains_reset_prop() { - local NAME=$1 - local CONTAINS=$2 - local NEWVAL=$3 - case "$("${'$'}RESETPROP_BIN" ${'$'}NAME)" in - *"${'$'}CONTAINS"*) "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}NEWVAL ;; - esac - } - """.trimIndent()) - appendLine() - appendLine("sleep 30") - appendLine() - appendLine("\"${'$'}RESETPROP_BIN\" -w sys.boot_completed 0") - - // 添加所有系统属性重置 - val systemProps = listOf( - "ro.boot.vbmeta.invalidate_on_error" to "yes", - "ro.boot.vbmeta.avb_version" to "1.2", - "ro.boot.vbmeta.hash_alg" to "sha256", - "ro.boot.vbmeta.size" to "19968", - "ro.boot.vbmeta.device_state" to "locked", - "ro.boot.verifiedbootstate" to "green", - "ro.boot.flash.locked" to "1", - "ro.boot.veritymode" to "enforcing", - "ro.boot.warranty_bit" to "0", - "ro.warranty_bit" to "0", - "ro.debuggable" to "0", - "ro.force.debuggable" to "0", - "ro.secure" to "1", - "ro.adb.secure" to "1", - "ro.build.type" to "user", - "ro.build.tags" to "release-keys", - "ro.vendor.boot.warranty_bit" to "0", - "ro.vendor.warranty_bit" to "0", - "vendor.boot.vbmeta.device_state" to "locked", - "vendor.boot.verifiedbootstate" to "green", - "sys.oem_unlock_allowed" to "0", - "ro.secureboot.lockstate" to "locked", - "ro.boot.realmebootstate" to "green", - "ro.boot.realme.lockstate" to "1", - "ro.crypto.state" to "encrypted" - ) - - systemProps.forEach { (prop, value) -> - when { - prop.startsWith("ro.boot.vbmeta") && prop.endsWith("_on_error") -> - appendLine("check_missing_prop \"$prop\" \"$value\"") - prop.contains("device_state") || prop.contains("verifiedbootstate") -> - appendLine("check_missing_match_prop \"$prop\" \"$value\"") - else -> - appendLine("check_reset_prop \"$prop\" \"$value\"") - } - } - - appendLine() - appendLine("# Hide adb debugging traces") - appendLine("resetprop \"sys.usb.adb.disabled\" \" \"") - appendLine() - - appendLine("# Hide recovery boot mode") - appendLine("contains_reset_prop \"ro.bootmode\" \"recovery\" \"unknown\"") - appendLine("contains_reset_prop \"ro.boot.bootmode\" \"recovery\" \"unknown\"") - appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"") - appendLine() - - appendLine("# Hide cloudphone detection") - appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"") - appendLine() - } - - // 清理残留脚本生成 - private fun StringBuilder.generateCleanupResidueSection() { - appendLine("# 清理工具残留文件") - appendLine("echo \"$(get_current_time): 开始清理工具残留\" >> \"${'$'}LOG_FILE\"") - appendLine() - - // 定义清理函数 - appendLine(""" - cleanup_path() { - local path="$1" - local desc="$2" - local current="$3" - local total="$4" - - if [ -n "${'$'}desc" ]; then - echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path (${'$'}desc)" >> "${'$'}LOG_FILE" - else - echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path" >> "${'$'}LOG_FILE" - fi - - if rm -rf "${'$'}path" 2>/dev/null; then - echo "$(get_current_time): ✓ 成功清理: ${'$'}path" >> "${'$'}LOG_FILE" - else - echo "$(get_current_time): ✗ 清理失败或不存在: ${'$'}path" >> "${'$'}LOG_FILE" - fi - } - """.trimIndent()) - - appendLine() - appendLine("# 开始清理各种工具残留") - appendLine("TOTAL=33") - appendLine() - - val cleanupPaths = listOf( - "/data/local/stryker/" to "Stryker残留", - "/data/system/AppRetention" to "AppRetention残留", - "/data/local/tmp/luckys" to "Luck Tool残留", - "/data/local/tmp/HyperCeiler" to "西米露残留", - "/data/local/tmp/simpleHook" to "simple Hook残留", - "/data/local/tmp/DisabledAllGoogleServices" to "谷歌省电模块残留", - "/data/local/MIO" to "解包软件", - "/data/DNA" to "解包软件", - "/data/local/tmp/cleaner_starter" to "质感清理残留", - "/data/local/tmp/byyang" to "", - "/data/local/tmp/mount_mask" to "", - "/data/local/tmp/mount_mark" to "", - "/data/local/tmp/scriptTMP" to "", - "/data/local/luckys" to "", - "/data/local/tmp/horae_control.log" to "", - "/data/gpu_freq_table.conf" to "", - "/storage/emulated/0/Download/advanced/" to "", - "/storage/emulated/0/Documents/advanced/" to "爱玩机", - "/storage/emulated/0/Android/naki/" to "旧版asoulopt", - "/data/swap_config.conf" to "scene附加模块2", - "/data/local/tmp/resetprop" to "", - "/dev/cpuset/AppOpt/" to "AppOpt模块", - "/storage/emulated/0/Android/Clash/" to "Clash for Magisk模块", - "/storage/emulated/0/Android/Yume-Yunyun/" to "网易云后台优化模块", - "/data/local/tmp/Surfing_update" to "Surfing模块缓存", - "/data/encore/custom_default_cpu_gov" to "encore模块", - "/data/encore/default_cpu_gov" to "encore模块", - "/data/local/tmp/yshell" to "", - "/data/local/tmp/encore_logo.png" to "", - "/storage/emulated/legacy/" to "", - "/storage/emulated/elgg/" to "", - "/data/system/junge/" to "", - "/data/local/tmp/mount_namespace" to "挂载命名空间残留" - ) - - cleanupPaths.forEachIndexed { index, (path, desc) -> - val current = index + 1 - appendLine("cleanup_path '$path' '$desc' $current \$TOTAL") - } - - appendLine() - appendLine("echo \"$(get_current_time): 工具残留清理完成\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - - /** - * 生成post-fs-data.sh脚本内容 - */ - private fun generatePostFsDataScript(config: SuSFSManager.ModuleConfig): String { - return buildString { - appendLine("#!/system/bin/sh") - appendLine("# SuSFS Post-FS-Data Script") - appendLine("# 在文件系统挂载后但在系统完全启动前执行") - appendLine() - appendLine(generateLogSetup("susfs_post_fs_data.log")) - appendLine() - appendLine(generateBinaryCheck(config.targetPath)) - appendLine() - appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"${'$'}LOG_FILE\"") - appendLine() - - // 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行 - if (config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) { - appendLine("# 设置uname和构建时间") - appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'") - appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - - generateUmountZygoteIsoServiceSection(config.umountForZygoteIsoService, config.support158) - - // 添加AVC日志欺骗设置 - generateAvcLogSpoofingSection(config.enableAvcLogSpoofing) - - appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"${'$'}LOG_FILE\"") - } - } - - // 添加新的生成方法 - private fun StringBuilder.generateUmountZygoteIsoServiceSection(umountForZygoteIsoService: Boolean, support158: Boolean) { - if (support158) { - appendLine("# 设置Zygote隔离服务卸载状态") - val umountValue = if (umountForZygoteIsoService) 1 else 0 - appendLine("\"${'$'}SUSFS_BIN\" umount_for_zygote_iso_service $umountValue") - appendLine("echo \"$(get_current_time): Zygote隔离服务卸载设置为: ${if (umountForZygoteIsoService) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - } - - /** - * 生成post-mount.sh脚本内容 - */ - private fun generatePostMountScript(config: SuSFSManager.ModuleConfig): String { - return buildString { - appendLine("#!/system/bin/sh") - appendLine("# SuSFS Post-Mount Script") - appendLine("# 在所有分区挂载完成后执行") - appendLine() - appendLine(generateLogSetup("susfs_post_mount.log")) - appendLine() - appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"${'$'}LOG_FILE\"") - appendLine() - appendLine(generateBinaryCheck(config.targetPath)) - appendLine() - - // 添加SUS挂载 - if (config.susMounts.isNotEmpty()) { - appendLine("# 添加SUS挂载") - config.susMounts.forEach { mount -> - appendLine("\"${'$'}SUSFS_BIN\" add_sus_mount '$mount'") - appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"${'$'}LOG_FILE\"") - } - appendLine() - } - - // 添加尝试卸载 - if (config.tryUmounts.isNotEmpty()) { - appendLine("# 添加尝试卸载") - config.tryUmounts.forEach { umount -> - val parts = umount.split("|") - if (parts.size == 2) { - val path = parts[0] - val mode = parts[1] - appendLine("\"${'$'}SUSFS_BIN\" add_try_umount '$path' $mode") - appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"${'$'}LOG_FILE\"") - } - } - appendLine() - } - - appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"${'$'}LOG_FILE\"") - } - } - - /** - * 生成boot-completed.sh脚本内容 - */ - @SuppressLint("SdCardPath") - private fun generateBootCompletedScript(config: SuSFSManager.ModuleConfig): String { - return buildString { - appendLine("#!/system/bin/sh") - appendLine("# SuSFS Boot-Completed Script") - appendLine("# 在系统完全启动后执行") - appendLine() - appendLine(generateLogSetup("susfs_boot_completed.log")) - appendLine() - appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"${'$'}LOG_FILE\"") - appendLine() - appendLine(generateBinaryCheck(config.targetPath)) - appendLine() - - // 仅在支持隐藏挂载功能时执行相关配置 - if (config.support158) { - // SUS挂载隐藏控制 - val hideValue = if (config.hideSusMountsForAllProcs) 1 else 0 - appendLine("# 设置SUS挂载隐藏控制") - appendLine("\"${'$'}SUSFS_BIN\" hide_sus_mnts_for_all_procs $hideValue") - appendLine("echo \"$(get_current_time): SUS挂载隐藏控制设置为: ${if (config.hideSusMountsForAllProcs) "对所有进程隐藏" else "仅对非KSU进程隐藏"}\" >> \"${'$'}LOG_FILE\"") - appendLine() - - // 路径设置和SUS路径设置 - if (config.susPaths.isNotEmpty() || config.susLoopPaths.isNotEmpty()) { - generatePathSettingSection(config.androidDataPath, config.sdcardPath) - appendLine() - - // 添加普通SUS路径 - if (config.susPaths.isNotEmpty()) { - generateSusPathsSection(config.susPaths) - } - - // 添加循环SUS路径 - if (config.susLoopPaths.isNotEmpty()) { - generateSusLoopPathsSection(config.susLoopPaths) - } - - if (config.susMaps.isNotEmpty()) { - generateSusMapsSection(config.susMaps) - } - } - } - - appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"") - } - } - - private fun StringBuilder.generateSusMapsSection(susMaps: Set) { - if (susMaps.isNotEmpty()) { - appendLine("# 添加SUS映射") - susMaps.forEach { map -> - appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'") - appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"") - } - appendLine() - } - } - - @SuppressLint("SdCardPath") - private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) { - appendLine("# 路径配置") - appendLine("# 设置Android Data路径") - appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done") - appendLine("sleep 60") - appendLine() - appendLine("\"${'$'}SUSFS_BIN\" set_android_data_root_path '$androidDataPath'") - appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"${'$'}LOG_FILE\"") - appendLine() - appendLine("# 设置SD卡路径") - appendLine("\"${'$'}SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'") - appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"${'$'}LOG_FILE\"") - appendLine() - } - - /** - * 生成module.prop文件内容 - */ - fun generateModuleProp(moduleId: String): String { - val moduleVersion = "v1.0.2" - val moduleVersionCode = "1002" - - return """ - id=$moduleId - name=SuSFS Manager - version=$moduleVersion - versionCode=$moduleVersionCode - author=ShirkNeko - description=SuSFS Manager Auto Configuration Module (自动生成请不要手动卸载或删除该模块! / Automatically generated Please do not manually uninstall or delete the module!) - updateJson= - """.trimIndent() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt deleted file mode 100644 index 27578522..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import android.content.Context -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.CardDefaults -import androidx.compose.runtime.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -@Stable -object CardConfig { - // 卡片透明度 - var cardAlpha by mutableFloatStateOf(1f) - internal set - // 卡片亮度 - var cardDim by mutableFloatStateOf(0f) - internal set - // 卡片阴影 - var cardElevation by mutableStateOf(0.dp) - internal set - - // 功能开关 - var isShadowEnabled by mutableStateOf(true) - internal set - var isCustomBackgroundEnabled by mutableStateOf(false) - internal set - - var isCustomAlphaSet by mutableStateOf(false) - internal set - var isCustomDimSet by mutableStateOf(false) - internal set - var isUserDarkModeEnabled by mutableStateOf(false) - internal set - var isUserLightModeEnabled by mutableStateOf(false) - internal set - - // 配置键名 - private object Keys { - const val CARD_ALPHA = "card_alpha" - const val CARD_DIM = "card_dim" - const val CUSTOM_BACKGROUND_ENABLED = "custom_background_enabled" - const val IS_SHADOW_ENABLED = "is_shadow_enabled" - const val IS_CUSTOM_ALPHA_SET = "is_custom_alpha_set" - const val IS_CUSTOM_DIM_SET = "is_custom_dim_set" - const val IS_USER_DARK_MODE_ENABLED = "is_user_dark_mode_enabled" - const val IS_USER_LIGHT_MODE_ENABLED = "is_user_light_mode_enabled" - } - - fun updateAlpha(alpha: Float, isCustom: Boolean = true) { - cardAlpha = alpha.coerceIn(0f, 1f) - if (isCustom) isCustomAlphaSet = true - } - - fun updateDim(dim: Float, isCustom: Boolean = true) { - cardDim = dim.coerceIn(0f, 1f) - if (isCustom) isCustomDimSet = true - } - - fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) { - isShadowEnabled = enabled - cardElevation = if (enabled) elevation else cardElevation - } - - fun updateBackground(enabled: Boolean) { - isCustomBackgroundEnabled = enabled - // 自定义背景时自动禁用阴影以获得更好的视觉效果 - if (enabled) { - updateShadow(false) - } - } - - fun updateThemePreference(darkMode: Boolean?, lightMode: Boolean?) { - isUserDarkModeEnabled = darkMode ?: false - isUserLightModeEnabled = lightMode ?: false - } - - fun reset() { - cardAlpha = 1f - cardDim = 0f - cardElevation = 0.dp - isShadowEnabled = true - isCustomBackgroundEnabled = false - isCustomAlphaSet = false - isCustomDimSet = false - isUserDarkModeEnabled = false - isUserLightModeEnabled = false - } - - fun setThemeDefaults(isDarkMode: Boolean) { - if (!isCustomAlphaSet) { - updateAlpha(if (isDarkMode) 0.88f else 1f, false) - } - if (!isCustomDimSet) { - updateDim(if (isDarkMode) 0.25f else 0f, false) - } - // 暗色模式下默认启用轻微阴影 - if (isDarkMode && !isCustomBackgroundEnabled) { - updateShadow(true, 2.dp) - } - } - - fun save(context: Context) { - val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) - prefs.edit().apply { - putFloat(Keys.CARD_ALPHA, cardAlpha) - putFloat(Keys.CARD_DIM, cardDim) - putBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, isCustomBackgroundEnabled) - putBoolean(Keys.IS_SHADOW_ENABLED, isShadowEnabled) - putBoolean(Keys.IS_CUSTOM_ALPHA_SET, isCustomAlphaSet) - putBoolean(Keys.IS_CUSTOM_DIM_SET, isCustomDimSet) - putBoolean(Keys.IS_USER_DARK_MODE_ENABLED, isUserDarkModeEnabled) - putBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, isUserLightModeEnabled) - apply() - } - } - - fun load(context: Context) { - val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) - cardAlpha = prefs.getFloat(Keys.CARD_ALPHA, 1f).coerceIn(0f, 1f) - cardDim = prefs.getFloat(Keys.CARD_DIM, 0f).coerceIn(0f, 1f) - isCustomBackgroundEnabled = prefs.getBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, false) - isShadowEnabled = prefs.getBoolean(Keys.IS_SHADOW_ENABLED, true) - isCustomAlphaSet = prefs.getBoolean(Keys.IS_CUSTOM_ALPHA_SET, false) - isCustomDimSet = prefs.getBoolean(Keys.IS_CUSTOM_DIM_SET, false) - isUserDarkModeEnabled = prefs.getBoolean(Keys.IS_USER_DARK_MODE_ENABLED, false) - isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false) - - // 应用阴影设置 - updateShadow(isShadowEnabled, if (isShadowEnabled) cardElevation else 0.dp) - } - - @Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)")) - fun updateShadowEnabled(enabled: Boolean) { - updateShadow(enabled) - } -} - -object CardStyleProvider { - - @Composable - fun getCardColors(originalColor: Color) = CardDefaults.cardColors( - containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), - contentColor = determineContentColor(originalColor), - disabledContainerColor = originalColor.copy(alpha = CardConfig.cardAlpha * 0.38f), - disabledContentColor = determineContentColor(originalColor).copy(alpha = 0.38f) - ) - - @Composable - fun getCardElevation() = CardDefaults.cardElevation( - defaultElevation = CardConfig.cardElevation, - pressedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 0).dp - } else 0.dp, - focusedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 0).dp - } else 0.dp, - hoveredElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 0).dp - } else 0.dp, - draggedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 0).dp - } else 0.dp, - disabledElevation = 0.dp - ) - - @Composable - private fun determineContentColor(originalColor: Color): Color { - val isDarkTheme = isSystemInDarkTheme() - - return when { - ThemeConfig.isThemeChanging -> { - if (isDarkTheme) Color.White else Color.Black - } - CardConfig.isUserLightModeEnabled -> Color.Black - CardConfig.isUserDarkModeEnabled -> Color.White - else -> { - val luminance = originalColor.luminance() - val threshold = if (isDarkTheme) 0.4f else 0.6f - if (luminance > threshold) Color.Black else Color.White - } - } - } -} - -// 向后兼容 -@Composable -fun getCardColors(originalColor: Color) = CardStyleProvider.getCardColors(originalColor) - -@Composable -fun getCardElevation() = CardStyleProvider.getCardElevation() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt deleted file mode 100644 index 52d1367e..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt +++ /dev/null @@ -1,615 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import androidx.compose.ui.graphics.Color - -sealed class ThemeColors { - // 浅色 - abstract val primaryLight: Color - abstract val onPrimaryLight: Color - abstract val primaryContainerLight: Color - abstract val onPrimaryContainerLight: Color - abstract val secondaryLight: Color - abstract val onSecondaryLight: Color - abstract val secondaryContainerLight: Color - abstract val onSecondaryContainerLight: Color - abstract val tertiaryLight: Color - abstract val onTertiaryLight: Color - abstract val tertiaryContainerLight: Color - abstract val onTertiaryContainerLight: Color - abstract val errorLight: Color - abstract val onErrorLight: Color - abstract val errorContainerLight: Color - abstract val onErrorContainerLight: Color - abstract val backgroundLight: Color - abstract val onBackgroundLight: Color - abstract val surfaceLight: Color - abstract val onSurfaceLight: Color - abstract val surfaceVariantLight: Color - abstract val onSurfaceVariantLight: Color - abstract val outlineLight: Color - abstract val outlineVariantLight: Color - abstract val scrimLight: Color - abstract val inverseSurfaceLight: Color - abstract val inverseOnSurfaceLight: Color - abstract val inversePrimaryLight: Color - abstract val surfaceDimLight: Color - abstract val surfaceBrightLight: Color - abstract val surfaceContainerLowestLight: Color - abstract val surfaceContainerLowLight: Color - abstract val surfaceContainerLight: Color - abstract val surfaceContainerHighLight: Color - abstract val surfaceContainerHighestLight: Color - // 深色 - abstract val primaryDark: Color - abstract val onPrimaryDark: Color - abstract val primaryContainerDark: Color - abstract val onPrimaryContainerDark: Color - abstract val secondaryDark: Color - abstract val onSecondaryDark: Color - abstract val secondaryContainerDark: Color - abstract val onSecondaryContainerDark: Color - abstract val tertiaryDark: Color - abstract val onTertiaryDark: Color - abstract val tertiaryContainerDark: Color - abstract val onTertiaryContainerDark: Color - abstract val errorDark: Color - abstract val onErrorDark: Color - abstract val errorContainerDark: Color - abstract val onErrorContainerDark: Color - abstract val backgroundDark: Color - abstract val onBackgroundDark: Color - abstract val surfaceDark: Color - abstract val onSurfaceDark: Color - abstract val surfaceVariantDark: Color - abstract val onSurfaceVariantDark: Color - abstract val outlineDark: Color - abstract val outlineVariantDark: Color - abstract val scrimDark: Color - abstract val inverseSurfaceDark: Color - abstract val inverseOnSurfaceDark: Color - abstract val inversePrimaryDark: Color - abstract val surfaceDimDark: Color - abstract val surfaceBrightDark: Color - abstract val surfaceContainerLowestDark: Color - abstract val surfaceContainerLowDark: Color - abstract val surfaceContainerDark: Color - abstract val surfaceContainerHighDark: Color - abstract val surfaceContainerHighestDark: Color - - // 默认主题 (蓝色) - object Default : ThemeColors() { - override val primaryLight = Color(0xFF415F91) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFD6E3FF) - override val onPrimaryContainerLight = Color(0xFF284777) - override val secondaryLight = Color(0xFF565F71) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFDAE2F9) - override val onSecondaryContainerLight = Color(0xFF3E4759) - override val tertiaryLight = Color(0xFF705575) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFFAD8FD) - override val onTertiaryContainerLight = Color(0xFF573E5C) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFF9F9FF) - override val onBackgroundLight = Color(0xFF191C20) - override val surfaceLight = Color(0xFFF9F9FF) - override val onSurfaceLight = Color(0xFF191C20) - override val surfaceVariantLight = Color(0xFFE0E2EC) - override val onSurfaceVariantLight = Color(0xFF44474E) - override val outlineLight = Color(0xFF74777F) - override val outlineVariantLight = Color(0xFFC4C6D0) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF2E3036) - override val inverseOnSurfaceLight = Color(0xFFF0F0F7) - override val inversePrimaryLight = Color(0xFFAAC7FF) - override val surfaceDimLight = Color(0xFFD9D9E0) - override val surfaceBrightLight = Color(0xFFF9F9FF) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFF3F3FA) - override val surfaceContainerLight = Color(0xFFEDEDF4) - override val surfaceContainerHighLight = Color(0xFFE7E8EE) - override val surfaceContainerHighestLight = Color(0xFFE2E2E9) - - override val primaryDark = Color(0xFFAAC7FF) - override val onPrimaryDark = Color(0xFF0A305F) - override val primaryContainerDark = Color(0xFF284777) - override val onPrimaryContainerDark = Color(0xFFD6E3FF) - override val secondaryDark = Color(0xFFBEC6DC) - override val onSecondaryDark = Color(0xFF283141) - override val secondaryContainerDark = Color(0xFF3E4759) - override val onSecondaryContainerDark = Color(0xFFDAE2F9) - override val tertiaryDark = Color(0xFFDDBCE0) - override val onTertiaryDark = Color(0xFF3F2844) - override val tertiaryContainerDark = Color(0xFF573E5C) - override val onTertiaryContainerDark = Color(0xFFFAD8FD) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF111318) - override val onBackgroundDark = Color(0xFFE2E2E9) - override val surfaceDark = Color(0xFF111318) - override val onSurfaceDark = Color(0xFFE2E2E9) - override val surfaceVariantDark = Color(0xFF44474E) - override val onSurfaceVariantDark = Color(0xFFC4C6D0) - override val outlineDark = Color(0xFF8E9099) - override val outlineVariantDark = Color(0xFF44474E) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFE2E2E9) - override val inverseOnSurfaceDark = Color(0xFF2E3036) - override val inversePrimaryDark = Color(0xFF415F91) - override val surfaceDimDark = Color(0xFF111318) - override val surfaceBrightDark = Color(0xFF37393E) - override val surfaceContainerLowestDark = Color(0xFF0C0E13) - override val surfaceContainerLowDark = Color(0xFF191C20) - override val surfaceContainerDark = Color(0xFF1D2024) - override val surfaceContainerHighDark = Color(0xFF282A2F) - override val surfaceContainerHighestDark = Color(0xFF33353A) - } - - // 绿色主题 - object Green : ThemeColors() { - override val primaryLight = Color(0xFF4C662B) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFCDEDA3) - override val onPrimaryContainerLight = Color(0xFF354E16) - override val secondaryLight = Color(0xFF586249) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFDCE7C8) - override val onSecondaryContainerLight = Color(0xFF404A33) - override val tertiaryLight = Color(0xFF386663) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFBCECE7) - override val onTertiaryContainerLight = Color(0xFF1F4E4B) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFF9FAEF) - override val onBackgroundLight = Color(0xFF1A1C16) - override val surfaceLight = Color(0xFFF9FAEF) - override val onSurfaceLight = Color(0xFF1A1C16) - override val surfaceVariantLight = Color(0xFFE1E4D5) - override val onSurfaceVariantLight = Color(0xFF44483D) - override val outlineLight = Color(0xFF75796C) - override val outlineVariantLight = Color(0xFFC5C8BA) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF2F312A) - override val inverseOnSurfaceLight = Color(0xFFF1F2E6) - override val inversePrimaryLight = Color(0xFFB1D18A) - override val surfaceDimLight = Color(0xFFDADBD0) - override val surfaceBrightLight = Color(0xFFF9FAEF) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFF3F4E9) - override val surfaceContainerLight = Color(0xFFEEEFE3) - override val surfaceContainerHighLight = Color(0xFFE8E9DE) - override val surfaceContainerHighestLight = Color(0xFFE2E3D8) - - override val primaryDark = Color(0xFFB1D18A) - override val onPrimaryDark = Color(0xFF1F3701) - override val primaryContainerDark = Color(0xFF354E16) - override val onPrimaryContainerDark = Color(0xFFCDEDA3) - override val secondaryDark = Color(0xFFBFCBAD) - override val onSecondaryDark = Color(0xFF2A331E) - override val secondaryContainerDark = Color(0xFF404A33) - override val onSecondaryContainerDark = Color(0xFFDCE7C8) - override val tertiaryDark = Color(0xFFA0D0CB) - override val onTertiaryDark = Color(0xFF003735) - override val tertiaryContainerDark = Color(0xFF1F4E4B) - override val onTertiaryContainerDark = Color(0xFFBCECE7) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF12140E) - override val onBackgroundDark = Color(0xFFE2E3D8) - override val surfaceDark = Color(0xFF12140E) - override val onSurfaceDark = Color(0xFFE2E3D8) - override val surfaceVariantDark = Color(0xFF44483D) - override val onSurfaceVariantDark = Color(0xFFC5C8BA) - override val outlineDark = Color(0xFF8F9285) - override val outlineVariantDark = Color(0xFF44483D) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFE2E3D8) - override val inverseOnSurfaceDark = Color(0xFF2F312A) - override val inversePrimaryDark = Color(0xFF4C662B) - override val surfaceDimDark = Color(0xFF12140E) - override val surfaceBrightDark = Color(0xFF383A32) - override val surfaceContainerLowestDark = Color(0xFF0C0F09) - override val surfaceContainerLowDark = Color(0xFF1A1C16) - override val surfaceContainerDark = Color(0xFF1E201A) - override val surfaceContainerHighDark = Color(0xFF282B24) - override val surfaceContainerHighestDark = Color(0xFF33362E) - } - - // 紫色主题 - object Purple : ThemeColors() { - override val primaryLight = Color(0xFF7C4E7E) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFFFD6FC) - override val onPrimaryContainerLight = Color(0xFF623765) - override val secondaryLight = Color(0xFF6C586B) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFF5DBF1) - override val onSecondaryContainerLight = Color(0xFF534152) - override val tertiaryLight = Color(0xFF825249) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFFFDAD4) - override val onTertiaryContainerLight = Color(0xFF673B33) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFFFF7FA) - override val onBackgroundLight = Color(0xFF1F1A1F) - override val surfaceLight = Color(0xFFFFF7FA) - override val onSurfaceLight = Color(0xFF1F1A1F) - override val surfaceVariantLight = Color(0xFFEDDFE8) - override val onSurfaceVariantLight = Color(0xFF4D444C) - override val outlineLight = Color(0xFF7F747C) - override val outlineVariantLight = Color(0xFFD0C3CC) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF352F34) - override val inverseOnSurfaceLight = Color(0xFFF9EEF4) - override val inversePrimaryLight = Color(0xFFECB4EC) - override val surfaceDimLight = Color(0xFFE2D7DE) - override val surfaceBrightLight = Color(0xFFFFF7FA) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFFCF0F7) - override val surfaceContainerLight = Color(0xFFF6EBF2) - override val surfaceContainerHighLight = Color(0xFFF0E5EC) - override val surfaceContainerHighestLight = Color(0xFFEBDFE6) - - override val primaryDark = Color(0xFFECB4EC) - override val onPrimaryDark = Color(0xFF49204D) - override val primaryContainerDark = Color(0xFF623765) - override val onPrimaryContainerDark = Color(0xFFFFD6FC) - override val secondaryDark = Color(0xFFD8BFD5) - override val onSecondaryDark = Color(0xFF3B2B3B) - override val secondaryContainerDark = Color(0xFF534152) - override val onSecondaryContainerDark = Color(0xFFF5DBF1) - override val tertiaryDark = Color(0xFFF6B8AD) - override val onTertiaryDark = Color(0xFF4C251F) - override val tertiaryContainerDark = Color(0xFF673B33) - override val onTertiaryContainerDark = Color(0xFFFFDAD4) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF171216) - override val onBackgroundDark = Color(0xFFEBDFE6) - override val surfaceDark = Color(0xFF171216) - override val onSurfaceDark = Color(0xFFEBDFE6) - override val surfaceVariantDark = Color(0xFF4D444C) - override val onSurfaceVariantDark = Color(0xFFD0C3CC) - override val outlineDark = Color(0xFF998D96) - override val outlineVariantDark = Color(0xFF4D444C) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFEBDFE6) - override val inverseOnSurfaceDark = Color(0xFF352F34) - override val inversePrimaryDark = Color(0xFF7C4E7E) - override val surfaceDimDark = Color(0xFF171216) - override val surfaceBrightDark = Color(0xFF3E373D) - override val surfaceContainerLowestDark = Color(0xFF110D11) - override val surfaceContainerLowDark = Color(0xFF1F1A1F) - override val surfaceContainerDark = Color(0xFF231E23) - override val surfaceContainerHighDark = Color(0xFF2E282D) - override val surfaceContainerHighestDark = Color(0xFF393338) - } - - // 橙色主题 - object Orange : ThemeColors() { - override val primaryLight = Color(0xFF8B4F24) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFFFDCC7) - override val onPrimaryContainerLight = Color(0xFF6E390E) - override val secondaryLight = Color(0xFF755846) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFFFDCC7) - override val onSecondaryContainerLight = Color(0xFF5B4130) - override val tertiaryLight = Color(0xFF865219) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFFFDCBF) - override val onTertiaryContainerLight = Color(0xFF6A3B01) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFFFF8F5) - override val onBackgroundLight = Color(0xFF221A15) - override val surfaceLight = Color(0xFFFFF8F5) - override val onSurfaceLight = Color(0xFF221A15) - override val surfaceVariantLight = Color(0xFFF4DED3) - override val onSurfaceVariantLight = Color(0xFF52443C) - override val outlineLight = Color(0xFF84746A) - override val outlineVariantLight = Color(0xFFD7C3B8) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF382E29) - override val inverseOnSurfaceLight = Color(0xFFFFEDE5) - override val inversePrimaryLight = Color(0xFFFFB787) - override val surfaceDimLight = Color(0xFFE7D7CE) - override val surfaceBrightLight = Color(0xFFFFF8F5) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFFFF1EA) - override val surfaceContainerLight = Color(0xFFFCEBE2) - override val surfaceContainerHighLight = Color(0xFFF6E5DC) - override val surfaceContainerHighestLight = Color(0xFFF0DFD7) - - override val primaryDark = Color(0xFFFFB787) - override val onPrimaryDark = Color(0xFF502400) - override val primaryContainerDark = Color(0xFF6E390E) - override val onPrimaryContainerDark = Color(0xFFFFDCC7) - override val secondaryDark = Color(0xFFE5BFA8) - override val onSecondaryDark = Color(0xFF422B1B) - override val secondaryContainerDark = Color(0xFF5B4130) - override val onSecondaryContainerDark = Color(0xFFFFDCC7) - override val tertiaryDark = Color(0xFFFDB876) - override val onTertiaryDark = Color(0xFF4B2800) - override val tertiaryContainerDark = Color(0xFF6A3B01) - override val onTertiaryContainerDark = Color(0xFFFFDCBF) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF19120D) - override val onBackgroundDark = Color(0xFFF0DFD7) - override val surfaceDark = Color(0xFF19120D) - override val onSurfaceDark = Color(0xFFF0DFD7) - override val surfaceVariantDark = Color(0xFF52443C) - override val onSurfaceVariantDark = Color(0xFFD7C3B8) - override val outlineDark = Color(0xFF9F8D83) - override val outlineVariantDark = Color(0xFF52443C) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFF0DFD7) - override val inverseOnSurfaceDark = Color(0xFF382E29) - override val inversePrimaryDark = Color(0xFF8B4F24) - override val surfaceDimDark = Color(0xFF19120D) - override val surfaceBrightDark = Color(0xFF413731) - override val surfaceContainerLowestDark = Color(0xFF140D08) - override val surfaceContainerLowDark = Color(0xFF221A15) - override val surfaceContainerDark = Color(0xFF261E19) - override val surfaceContainerHighDark = Color(0xFF312823) - override val surfaceContainerHighestDark = Color(0xFF3D332D) - } - - // 粉色主题 - object Pink : ThemeColors() { - override val primaryLight = Color(0xFF8C4A60) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFFFD9E2) - override val onPrimaryContainerLight = Color(0xFF703348) - override val secondaryLight = Color(0xFF8B4A62) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFFFD9E3) - override val onSecondaryContainerLight = Color(0xFF6F334B) - override val tertiaryLight = Color(0xFF8B4A62) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFFFD9E3) - override val onTertiaryContainerLight = Color(0xFF6F334B) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFFFF8F8) - override val onBackgroundLight = Color(0xFF22191B) - override val surfaceLight = Color(0xFFFFF8F8) - override val onSurfaceLight = Color(0xFF22191B) - override val surfaceVariantLight = Color(0xFFF2DDE1) - override val onSurfaceVariantLight = Color(0xFF514346) - override val outlineLight = Color(0xFF837377) - override val outlineVariantLight = Color(0xFFD5C2C5) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF372E30) - override val inverseOnSurfaceLight = Color(0xFFFDEDEF) - override val inversePrimaryLight = Color(0xFFFFB1C7) - override val surfaceDimLight = Color(0xFFE6D6D9) - override val surfaceBrightLight = Color(0xFFFFF8F8) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFFFF0F2) - override val surfaceContainerLight = Color(0xFFFBEAED) - override val surfaceContainerHighLight = Color(0xFFF5E4E7) - override val surfaceContainerHighestLight = Color(0xFFEFDFE1) - - override val primaryDark = Color(0xFFFFB1C7) - override val onPrimaryDark = Color(0xFF541D32) - override val primaryContainerDark = Color(0xFF703348) - override val onPrimaryContainerDark = Color(0xFFFFD9E2) - override val secondaryDark = Color(0xFFFFB0CB) - override val onSecondaryDark = Color(0xFF541D34) - override val secondaryContainerDark = Color(0xFF6F334B) - override val onSecondaryContainerDark = Color(0xFFFFD9E3) - override val tertiaryDark = Color(0xFFFFB0CB) - override val onTertiaryDark = Color(0xFF541D34) - override val tertiaryContainerDark = Color(0xFF6F334B) - override val onTertiaryContainerDark = Color(0xFFFFD9E3) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF191113) - override val onBackgroundDark = Color(0xFFEFDFE1) - override val surfaceDark = Color(0xFF191113) - override val onSurfaceDark = Color(0xFFEFDFE1) - override val surfaceVariantDark = Color(0xFF514346) - override val onSurfaceVariantDark = Color(0xFFD5C2C5) - override val outlineDark = Color(0xFF9E8C90) - override val outlineVariantDark = Color(0xFF514346) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFEFDFE1) - override val inverseOnSurfaceDark = Color(0xFF372E30) - override val inversePrimaryDark = Color(0xFF8C4A60) - override val surfaceDimDark = Color(0xFF191113) - override val surfaceBrightDark = Color(0xFF413739) - override val surfaceContainerLowestDark = Color(0xFF140C0E) - override val surfaceContainerLowDark = Color(0xFF22191B) - override val surfaceContainerDark = Color(0xFF261D1F) - override val surfaceContainerHighDark = Color(0xFF31282A) - override val surfaceContainerHighestDark = Color(0xFF3C3234) - } - - // 灰色主题 - object Gray : ThemeColors() { - override val primaryLight = Color(0xFF5B5C5C) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFF747474) - override val onPrimaryContainerLight = Color(0xFFFEFCFC) - override val secondaryLight = Color(0xFF5F5E5E) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFE4E2E1) - override val onSecondaryContainerLight = Color(0xFF656464) - override val tertiaryLight = Color(0xFF5E5B5D) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFF777375) - override val onTertiaryContainerLight = Color(0xFFFFFBFF) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFFCF8F8) - override val onBackgroundLight = Color(0xFF1C1B1B) - override val surfaceLight = Color(0xFFFCF8F8) - override val onSurfaceLight = Color(0xFF1C1B1B) - override val surfaceVariantLight = Color(0xFFE0E3E3) - override val onSurfaceVariantLight = Color(0xFF444748) - override val outlineLight = Color(0xFF747878) - override val outlineVariantLight = Color(0xFFC4C7C7) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF313030) - override val inverseOnSurfaceLight = Color(0xFFF4F0EF) - override val inversePrimaryLight = Color(0xFFC7C6C6) - override val surfaceDimLight = Color(0xFFDDD9D8) - override val surfaceBrightLight = Color(0xFFFCF8F8) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFF7F3F2) - override val surfaceContainerLight = Color(0xFFF1EDEC) - override val surfaceContainerHighLight = Color(0xFFEBE7E7) - override val surfaceContainerHighestLight = Color(0xFFE5E2E1) - - override val primaryDark = Color(0xFFC7C6C6) - override val onPrimaryDark = Color(0xFF303031) - override val primaryContainerDark = Color(0xFF919190) - override val onPrimaryContainerDark = Color(0xFF161718) - override val secondaryDark = Color(0xFFC8C6C5) - override val onSecondaryDark = Color(0xFF303030) - override val secondaryContainerDark = Color(0xFF474746) - override val onSecondaryContainerDark = Color(0xFFB7B5B4) - override val tertiaryDark = Color(0xFFCAC5C7) - override val onTertiaryDark = Color(0xFF323031) - override val tertiaryContainerDark = Color(0xFF948F91) - override val onTertiaryContainerDark = Color(0xFF181718) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF141313) - override val onBackgroundDark = Color(0xFFE5E2E1) - override val surfaceDark = Color(0xFF141313) - override val onSurfaceDark = Color(0xFFE5E2E1) - override val surfaceVariantDark = Color(0xFF444748) - override val onSurfaceVariantDark = Color(0xFFC4C7C7) - override val outlineDark = Color(0xFF8E9192) - override val outlineVariantDark = Color(0xFF444748) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFE5E2E1) - override val inverseOnSurfaceDark = Color(0xFF313030) - override val inversePrimaryDark = Color(0xFF5E5E5E) - override val surfaceDimDark = Color(0xFF141313) - override val surfaceBrightDark = Color(0xFF3A3939) - override val surfaceContainerLowestDark = Color(0xFF0E0E0E) - override val surfaceContainerLowDark = Color(0xFF1C1B1B) - override val surfaceContainerDark = Color(0xFF201F1F) - override val surfaceContainerHighDark = Color(0xFF2A2A2A) - override val surfaceContainerHighestDark = Color(0xFF353434) - } - - // 黄色主题 - object Yellow : ThemeColors() { - override val primaryLight = Color(0xFF6D5E0F) - override val onPrimaryLight = Color(0xFFFFFFFF) - override val primaryContainerLight = Color(0xFFF8E288) - override val onPrimaryContainerLight = Color(0xFF534600) - override val secondaryLight = Color(0xFF6D5E0F) - override val onSecondaryLight = Color(0xFFFFFFFF) - override val secondaryContainerLight = Color(0xFFF7E388) - override val onSecondaryContainerLight = Color(0xFF534600) - override val tertiaryLight = Color(0xFF685F13) - override val onTertiaryLight = Color(0xFFFFFFFF) - override val tertiaryContainerLight = Color(0xFFF1E58A) - override val onTertiaryContainerLight = Color(0xFF4F4800) - override val errorLight = Color(0xFFBA1A1A) - override val onErrorLight = Color(0xFFFFFFFF) - override val errorContainerLight = Color(0xFFFFDAD6) - override val onErrorContainerLight = Color(0xFF93000A) - override val backgroundLight = Color(0xFFFFF9ED) - override val onBackgroundLight = Color(0xFF1E1C13) - override val surfaceLight = Color(0xFFFFF9ED) - override val onSurfaceLight = Color(0xFF1E1C13) - override val surfaceVariantLight = Color(0xFFE9E2D0) - override val onSurfaceVariantLight = Color(0xFF4B4739) - override val outlineLight = Color(0xFF7C7768) - override val outlineVariantLight = Color(0xFFCDC6B4) - override val scrimLight = Color(0xFF000000) - override val inverseSurfaceLight = Color(0xFF333027) - override val inverseOnSurfaceLight = Color(0xFFF7F0E2) - override val inversePrimaryLight = Color(0xFFDAC66F) - override val surfaceDimLight = Color(0xFFE0D9CC) - override val surfaceBrightLight = Color(0xFFFFF9ED) - override val surfaceContainerLowestLight = Color(0xFFFFFFFF) - override val surfaceContainerLowLight = Color(0xFFFAF3E5) - override val surfaceContainerLight = Color(0xFFF4EDDF) - override val surfaceContainerHighLight = Color(0xFFEEE8DA) - override val surfaceContainerHighestLight = Color(0xFFE8E2D4) - - override val primaryDark = Color(0xFFDAC66F) - override val onPrimaryDark = Color(0xFF393000) - override val primaryContainerDark = Color(0xFF534600) - override val onPrimaryContainerDark = Color(0xFFF8E288) - override val secondaryDark = Color(0xFFDAC76F) - override val onSecondaryDark = Color(0xFF393000) - override val secondaryContainerDark = Color(0xFF534600) - override val onSecondaryContainerDark = Color(0xFFF7E388) - override val tertiaryDark = Color(0xFFD4C871) - override val onTertiaryDark = Color(0xFF363100) - override val tertiaryContainerDark = Color(0xFF4F4800) - override val onTertiaryContainerDark = Color(0xFFF1E58A) - override val errorDark = Color(0xFFFFB4AB) - override val onErrorDark = Color(0xFF690005) - override val errorContainerDark = Color(0xFF93000A) - override val onErrorContainerDark = Color(0xFFFFDAD6) - override val backgroundDark = Color(0xFF15130B) - override val onBackgroundDark = Color(0xFFE8E2D4) - override val surfaceDark = Color(0xFF15130B) - override val onSurfaceDark = Color(0xFFE8E2D4) - override val surfaceVariantDark = Color(0xFF4B4739) - override val onSurfaceVariantDark = Color(0xFFCDC6B4) - override val outlineDark = Color(0xFF969080) - override val outlineVariantDark = Color(0xFF4B4739) - override val scrimDark = Color(0xFF000000) - override val inverseSurfaceDark = Color(0xFFE8E2D4) - override val inverseOnSurfaceDark = Color(0xFF333027) - override val inversePrimaryDark = Color(0xFF6D5E0F) - override val surfaceDimDark = Color(0xFF15130B) - override val surfaceBrightDark = Color(0xFF3C3930) - override val surfaceContainerLowestDark = Color(0xFF100E07) - override val surfaceContainerLowDark = Color(0xFF1E1C13) - override val surfaceContainerDark = Color(0xFF222017) - override val surfaceContainerHighDark = Color(0xFF2C2A21) - override val surfaceContainerHighestDark = Color(0xFF37352B) - } - - companion object { - fun fromName(name: String): ThemeColors = when (name.lowercase()) { - "green" -> Green - "purple" -> Purple - "orange" -> Orange - "pink" -> Pink - "gray" -> Gray - "yellow" -> Yellow - else -> Default - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt index 87ac86d8..a06cd460 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt @@ -1,593 +1,22 @@ package com.sukisu.ultra.ui.theme -import android.content.Context -import android.net.Uri -import android.os.Build -import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.SystemBarStyle -import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi -import androidx.compose.animation.core.* -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -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.graphics.graphicsLayer -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.zIndex -import androidx.core.content.edit -import androidx.core.net.toUri -import coil.compose.AsyncImagePainter -import coil.compose.rememberAsyncImagePainter -import com.sukisu.ultra.ui.theme.util.BackgroundTransformation -import com.sukisu.ultra.ui.theme.util.saveTransformedBackground -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.launch -import java.io.File -import java.io.FileOutputStream - -@Stable -object ThemeConfig { - // 主题状态 - var customBackgroundUri by mutableStateOf(null) - var forceDarkMode by mutableStateOf(null) - var currentTheme by mutableStateOf(ThemeColors.Default) - var useDynamicColor by mutableStateOf(false) - - // 背景状态 - var backgroundImageLoaded by mutableStateOf(false) - var isThemeChanging by mutableStateOf(false) - var preventBackgroundRefresh by mutableStateOf(false) - - // 主题变化检测 - private var lastDarkModeState: Boolean? = null - - fun detectThemeChange(currentDarkMode: Boolean): Boolean { - val hasChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode - lastDarkModeState = currentDarkMode - return hasChanged - } - - fun resetBackgroundState() { - if (!preventBackgroundRefresh) { - backgroundImageLoaded = false - } - isThemeChanging = true - } - - fun updateTheme( - theme: ThemeColors? = null, - dynamicColor: Boolean? = null, - darkMode: Boolean? = null - ) { - theme?.let { currentTheme = it } - dynamicColor?.let { useDynamicColor = it } - darkMode?.let { forceDarkMode = it } - } - - fun reset() { - customBackgroundUri = null - forceDarkMode = null - currentTheme = ThemeColors.Default - useDynamicColor = false - backgroundImageLoaded = false - isThemeChanging = false - preventBackgroundRefresh = false - lastDarkModeState = null - } -} - -object ThemeManager { - private const val PREFS_NAME = "theme_prefs" - - fun saveThemeMode(context: Context, forceDark: Boolean?) { - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { - putString("theme_mode", when (forceDark) { - true -> "dark" - false -> "light" - null -> "system" - }) - } - ThemeConfig.forceDarkMode = forceDark - } - - fun loadThemeMode(context: Context) { - val mode = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - .getString("theme_mode", "system") - - ThemeConfig.forceDarkMode = when (mode) { - "dark" -> true - "light" -> false - else -> null - } - } - - fun saveThemeColors(context: Context, themeName: String) { - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { - putString("theme_colors", themeName) - } - ThemeConfig.currentTheme = ThemeColors.fromName(themeName) - } - - fun loadThemeColors(context: Context) { - val themeName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - .getString("theme_colors", "default") ?: "default" - ThemeConfig.currentTheme = ThemeColors.fromName(themeName) - } - - fun saveDynamicColorState(context: Context, enabled: Boolean) { - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { - putBoolean("use_dynamic_color", enabled) - } - ThemeConfig.useDynamicColor = enabled - } - - - fun loadDynamicColorState(context: Context) { - val enabled = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - .getBoolean("use_dynamic_color", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - ThemeConfig.useDynamicColor = enabled - } -} - -object BackgroundManager { - private const val TAG = "BackgroundManager" - - fun saveAndApplyCustomBackground( - context: Context, - uri: Uri, - transformation: BackgroundTransformation? = null - ) { - try { - val finalUri = if (transformation != null) { - context.saveTransformedBackground(uri, transformation) - } else { - copyImageToInternalStorage(context, uri) - } - - saveBackgroundUri(context, finalUri) - ThemeConfig.customBackgroundUri = finalUri - CardConfig.updateBackground(true) - resetBackgroundState(context) - - } catch (e: Exception) { - Log.e(TAG, "保存背景失败: ${e.message}", e) - } - } - - fun clearCustomBackground(context: Context) { - saveBackgroundUri(context, null) - ThemeConfig.customBackgroundUri = null - CardConfig.updateBackground(false) - resetBackgroundState(context) - } - - fun loadCustomBackground(context: Context) { - val uriString = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("custom_background", null) - - val newUri = uriString?.toUri() - val preventRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", false) - - ThemeConfig.preventBackgroundRefresh = preventRefresh - - if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { - Log.d(TAG, "加载自定义背景: $uriString") - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - CardConfig.updateBackground(newUri != null) - } - } - - private fun saveBackgroundUri(context: Context, uri: Uri?) { - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { - putString("custom_background", uri?.toString()) - putBoolean("prevent_background_refresh", false) - } - } - - private fun resetBackgroundState(context: Context) { - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { - putBoolean("prevent_background_refresh", false) - } - } - - private fun copyImageToInternalStorage(context: Context, uri: Uri): Uri? { - return try { - val inputStream = context.contentResolver.openInputStream(uri) ?: return null - val fileName = "custom_background_${System.currentTimeMillis()}.jpg" - val file = File(context.filesDir, fileName) - - FileOutputStream(file).use { outputStream -> - val buffer = ByteArray(8 * 1024) - var read: Int - while (inputStream.read(buffer).also { read = it } != -1) { - outputStream.write(buffer, 0, read) - } - outputStream.flush() - } - inputStream.close() - - Uri.fromFile(file) - } catch (e: Exception) { - Log.e(TAG, "复制图片失败: ${e.message}", e) - null - } - } -} +import androidx.compose.runtime.Composable +import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.darkColorScheme +import top.yukonga.miuix.kmp.theme.lightColorScheme @Composable fun KernelSUTheme( - darkTheme: Boolean = when(ThemeConfig.forceDarkMode) { - true -> true - false -> false - null -> isSystemInDarkTheme() - }, - dynamicColor: Boolean = ThemeConfig.useDynamicColor, + darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val context = LocalContext.current - val systemIsDark = isSystemInDarkTheme() - - // 初始化主题 - ThemeInitializer(context = context, systemIsDark = systemIsDark) - - // 创建颜色方案 - val colorScheme = createColorScheme(context, darkTheme, dynamicColor) - - // 系统栏样式 - SystemBarController(darkTheme) - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography - ) { - Box(modifier = Modifier.fillMaxSize()) { - // 背景层 - BackgroundLayer(darkTheme) - // 内容层 - Box(modifier = Modifier.fillMaxSize().zIndex(1f)) { - content() - } - } + val colorScheme = when { + darkTheme -> darkColorScheme() + else -> lightColorScheme() } -} - -@Composable -private fun ThemeInitializer(context: Context, systemIsDark: Boolean) { - val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) - val scope = rememberCoroutineScope() - - // 处理系统主题变化 - LaunchedEffect(systemIsDark, themeChanged) { - if (ThemeConfig.forceDarkMode == null && themeChanged) { - Log.d("ThemeSystem", "系统主题变化: $systemIsDark") - ThemeConfig.resetBackgroundState() - - if (!ThemeConfig.preventBackgroundRefresh) { - BackgroundManager.loadCustomBackground(context) - } - - CardConfig.apply { - load(context) - setThemeDefaults(systemIsDark) - save(context) - } - } - } - - // 初始加载配置 - LaunchedEffect(Unit) { - scope.launch { - ThemeManager.loadThemeMode(context) - ThemeManager.loadThemeColors(context) - ThemeManager.loadDynamicColorState(context) - CardConfig.load(context) - - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - BackgroundManager.loadCustomBackground(context) - } - } - } -} - -@Composable -private fun BackgroundLayer(darkTheme: Boolean) { - val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } - - LaunchedEffect(ThemeConfig.customBackgroundUri) { - backgroundUri.value = ThemeConfig.customBackgroundUri - } - - // 默认背景 - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-2f) - .background( - if (CardConfig.isCustomBackgroundEnabled) { - MaterialTheme.colorScheme.surfaceContainerLow - } else { - MaterialTheme.colorScheme.background - } - ) - ) - - // 自定义背景 - backgroundUri.value?.let { uri -> - CustomBackgroundLayer(uri = uri, darkTheme = darkTheme) - } -} - -@Composable -private fun CustomBackgroundLayer(uri: Uri, darkTheme: Boolean) { - val painter = rememberAsyncImagePainter( - model = uri, - onError = { error -> - Log.e("ThemeSystem", "背景加载失败: ${error.result.throwable.message}") - ThemeConfig.customBackgroundUri = null - }, - onSuccess = { - Log.d("ThemeSystem", "背景加载成功") - ThemeConfig.backgroundImageLoaded = true - ThemeConfig.isThemeChanging = false - } - ) - - val transition = updateTransition( - targetState = ThemeConfig.backgroundImageLoaded, - label = "backgroundTransition" - ) - - val alpha by transition.animateFloat( - label = "backgroundAlpha", - transitionSpec = { - spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ) - } - ) { loaded -> if (loaded) 1f else 0f } - - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .alpha(alpha) - ) { - // 背景图片 - Box( - modifier = Modifier - .fillMaxSize() - .paint(painter = painter, contentScale = ContentScale.Crop) - .graphicsLayer { - this.alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f - } - ) - - // 遮罩层 - BackgroundOverlay(darkTheme = darkTheme) - } -} - -@Composable -private fun BackgroundOverlay(darkTheme: Boolean) { - val dimFactor = CardConfig.cardDim - - // 主要遮罩层 - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (darkTheme) { - Color.Black.copy(alpha = 0.3f + dimFactor * 0.4f) - } else { - Color.White.copy(alpha = 0.05f + dimFactor * 0.3f) - } - ) - ) - - // 边缘渐变遮罩 - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color.Transparent, - if (darkTheme) { - Color.Black.copy(alpha = 0.2f + dimFactor * 0.2f) - } else { - Color.Black.copy(alpha = 0.05f + dimFactor * 0.1f) - } - ), - radius = 1000f - ) - ) + MiuixTheme( + colors = colorScheme, + content = content ) } - -@Composable -private fun createColorScheme( - context: Context, - darkTheme: Boolean, - dynamicColor: Boolean -): ColorScheme { - return when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (darkTheme) createDynamicDarkColorScheme(context) - else createDynamicLightColorScheme(context) - } - darkTheme -> createDarkColorScheme() - else -> createLightColorScheme() - } -} - -@Composable -private fun SystemBarController(darkMode: Boolean) { - val context = LocalContext.current - val activity = context as ComponentActivity - - SideEffect { - activity.enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( - Color.Transparent.toArgb(), - Color.Transparent.toArgb(), - ) { darkMode }, - navigationBarStyle = if (darkMode) { - SystemBarStyle.dark(Color.Transparent.toArgb()) - } else { - SystemBarStyle.light( - Color.Transparent.toArgb(), - Color.Transparent.toArgb() - ) - } - ) - } -} - -@RequiresApi(Build.VERSION_CODES.S) -@Composable -private fun createDynamicDarkColorScheme(context: Context): ColorScheme { - val scheme = dynamicDarkColorScheme(context) - return scheme.copy( - background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background, - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface, - onBackground = scheme.onBackground, - onSurface = scheme.onSurface - ) -} - -@RequiresApi(Build.VERSION_CODES.S) -@Composable -private fun createDynamicLightColorScheme(context: Context): ColorScheme { - val scheme = dynamicLightColorScheme(context) - return scheme.copy( - background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background, - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface, - onBackground = scheme.onBackground, - onSurface = scheme.onSurface - ) -} - -@Composable -private fun createDarkColorScheme() = darkColorScheme( - primary = ThemeConfig.currentTheme.primaryDark, - onPrimary = ThemeConfig.currentTheme.onPrimaryDark, - primaryContainer = ThemeConfig.currentTheme.primaryContainerDark, - onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerDark, - secondary = ThemeConfig.currentTheme.secondaryDark, - onSecondary = ThemeConfig.currentTheme.onSecondaryDark, - secondaryContainer = ThemeConfig.currentTheme.secondaryContainerDark, - onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerDark, - tertiary = ThemeConfig.currentTheme.tertiaryDark, - onTertiary = ThemeConfig.currentTheme.onTertiaryDark, - tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerDark, - onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerDark, - error = ThemeConfig.currentTheme.errorDark, - onError = ThemeConfig.currentTheme.onErrorDark, - errorContainer = ThemeConfig.currentTheme.errorContainerDark, - onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark, - background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark, - onBackground = ThemeConfig.currentTheme.onBackgroundDark, - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark, - onSurface = ThemeConfig.currentTheme.onSurfaceDark, - surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark, - onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark, - outline = ThemeConfig.currentTheme.outlineDark, - outlineVariant = ThemeConfig.currentTheme.outlineVariantDark, - scrim = ThemeConfig.currentTheme.scrimDark, - inverseSurface = ThemeConfig.currentTheme.inverseSurfaceDark, - inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceDark, - inversePrimary = ThemeConfig.currentTheme.inversePrimaryDark, - surfaceDim = ThemeConfig.currentTheme.surfaceDimDark, - surfaceBright = ThemeConfig.currentTheme.surfaceBrightDark, - surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestDark, - surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowDark, - surfaceContainer = ThemeConfig.currentTheme.surfaceContainerDark, - surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighDark, - surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark, -) - -@Composable -private fun createLightColorScheme() = lightColorScheme( - primary = ThemeConfig.currentTheme.primaryLight, - onPrimary = ThemeConfig.currentTheme.onPrimaryLight, - primaryContainer = ThemeConfig.currentTheme.primaryContainerLight, - onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerLight, - secondary = ThemeConfig.currentTheme.secondaryLight, - onSecondary = ThemeConfig.currentTheme.onSecondaryLight, - secondaryContainer = ThemeConfig.currentTheme.secondaryContainerLight, - onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerLight, - tertiary = ThemeConfig.currentTheme.tertiaryLight, - onTertiary = ThemeConfig.currentTheme.onTertiaryLight, - tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerLight, - onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerLight, - error = ThemeConfig.currentTheme.errorLight, - onError = ThemeConfig.currentTheme.onErrorLight, - errorContainer = ThemeConfig.currentTheme.errorContainerLight, - onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight, - background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight, - onBackground = ThemeConfig.currentTheme.onBackgroundLight, - surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight, - onSurface = ThemeConfig.currentTheme.onSurfaceLight, - surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight, - onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight, - outline = ThemeConfig.currentTheme.outlineLight, - outlineVariant = ThemeConfig.currentTheme.outlineVariantLight, - scrim = ThemeConfig.currentTheme.scrimLight, - inverseSurface = ThemeConfig.currentTheme.inverseSurfaceLight, - inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceLight, - inversePrimary = ThemeConfig.currentTheme.inversePrimaryLight, - surfaceDim = ThemeConfig.currentTheme.surfaceDimLight, - surfaceBright = ThemeConfig.currentTheme.surfaceBrightLight, - surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestLight, - surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowLight, - surfaceContainer = ThemeConfig.currentTheme.surfaceContainerLight, - surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighLight, - surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight, -) - -// 向后兼容 -@OptIn(DelicateCoroutinesApi::class) -fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { - kotlinx.coroutines.GlobalScope.launch { - BackgroundManager.saveAndApplyCustomBackground(this@saveAndApplyCustomBackground, uri, transformation) - } -} - -fun Context.saveCustomBackground(uri: Uri?) { - if (uri != null) { - saveAndApplyCustomBackground(uri) - } else { - BackgroundManager.clearCustomBackground(this) - } -} - -fun Context.saveThemeMode(forceDark: Boolean?) { - ThemeManager.saveThemeMode(this, forceDark) -} - - -fun Context.saveThemeColors(themeName: String) { - ThemeManager.saveThemeColors(this, themeName) -} - - -fun Context.saveDynamicColorState(enabled: Boolean) { - ThemeManager.saveDynamicColorState(this, enabled) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt deleted file mode 100644 index beefa2e2..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - // 大标题 - displayLarge = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), - displayMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - - // 标题 - headlineLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - - // 标题栏 - titleLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - - // 主体文字 - bodyLarge = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - - // 标签 - labelLarge = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) -) \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt deleted file mode 100644 index 803d1f04..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt +++ /dev/null @@ -1,411 +0,0 @@ -package com.sukisu.ultra.ui.theme.component - -import android.net.Uri -import androidx.compose.animation.core.* -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Fullscreen -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.util.BackgroundTransformation -import com.sukisu.ultra.ui.theme.util.saveTransformedBackground -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.max - -@Composable -fun ImageEditorDialog( - imageUri: Uri, - onDismiss: () -> Unit, - onConfirm: (Uri) -> Unit -) { - // 图像变换状态 - val transformState = remember { ImageTransformState() } - val context = LocalContext.current - val scope = rememberCoroutineScope() - - // 尺寸状态 - var imageSize by remember { mutableStateOf(Size.Zero) } - var screenSize by remember { mutableStateOf(Size.Zero) } - - // 动画状态 - val animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessMedium - ) - - val animatedScale by animateFloatAsState( - targetValue = transformState.scale, - animationSpec = animationSpec, - label = "ScaleAnimation" - ) - - val animatedOffsetX by animateFloatAsState( - targetValue = transformState.offsetX, - animationSpec = animationSpec, - label = "OffsetXAnimation" - ) - - val animatedOffsetY by animateFloatAsState( - targetValue = transformState.offsetY, - animationSpec = animationSpec, - label = "OffsetYAnimation" - ) - - // 工具函数 - val scaleToFullScreen = remember { - { - if (imageSize.height > 0 && screenSize.height > 0) { - val newScale = screenSize.height / imageSize.height - transformState.updateTransform(newScale, 0f, 0f) - } - } - } - - val saveImage: () -> Unit = remember { - { - scope.launch { - try { - val transformation = BackgroundTransformation( - transformState.scale, - transformState.offsetX, - transformState.offsetY - ) - val savedUri = context.saveTransformedBackground(imageUri, transformation) - savedUri?.let { onConfirm(it) } - } catch (_: Exception) { - } - } - } - } - - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false - ) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color.Black.copy(alpha = 0.9f), - Color.Black.copy(alpha = 0.95f) - ), - radius = 800f - ) - ) - .onSizeChanged { size -> - screenSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) { - // 图像显示区域 - ImageDisplayArea( - imageUri = imageUri, - animatedScale = animatedScale, - animatedOffsetX = animatedOffsetX, - animatedOffsetY = animatedOffsetY, - transformState = transformState, - onImageSizeChanged = { imageSize = it }, - modifier = Modifier.fillMaxSize() - ) - - // 顶部工具栏 - TopToolbar( - onDismiss = onDismiss, - onFullscreen = scaleToFullScreen, - onConfirm = saveImage, - modifier = Modifier.align(Alignment.TopCenter) - ) - - // 底部提示信息 - BottomHintCard( - modifier = Modifier.align(Alignment.BottomCenter) - ) - } - } -} - -/** - * 图像变换状态管理类 - */ -private class ImageTransformState { - var scale by mutableFloatStateOf(1f) - var offsetX by mutableFloatStateOf(0f) - var offsetY by mutableFloatStateOf(0f) - - private var lastScale = 1f - private var lastOffsetX = 0f - private var lastOffsetY = 0f - - fun updateTransform(newScale: Float, newOffsetX: Float, newOffsetY: Float) { - val scaleDiff = abs(newScale - lastScale) - val offsetXDiff = abs(newOffsetX - lastOffsetX) - val offsetYDiff = abs(newOffsetY - lastOffsetY) - - if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { - scale = newScale - offsetX = newOffsetX - offsetY = newOffsetY - lastScale = newScale - lastOffsetX = newOffsetX - lastOffsetY = newOffsetY - } - } - - fun resetToLast() { - scale = lastScale - offsetX = lastOffsetX - offsetY = lastOffsetY - } -} - -/** - * 图像显示区域组件 - */ -@Composable -private fun ImageDisplayArea( - imageUri: Uri, - animatedScale: Float, - animatedOffsetX: Float, - animatedOffsetY: Float, - transformState: ImageTransformState, - onImageSizeChanged: (Size) -> Unit, - modifier: Modifier = Modifier -) { - val scope = rememberCoroutineScope() - - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), - contentDescription = stringResource(R.string.settings_custom_background), - contentScale = ContentScale.Fit, - modifier = modifier - .graphicsLayer( - scaleX = animatedScale, - scaleY = animatedScale, - translationX = animatedOffsetX, - translationY = animatedOffsetY - ) - .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scope.launch { - try { - val newScale = (transformState.scale * zoom).coerceIn(0.5f, 3f) - val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) - val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) - - val newOffsetX = if (maxOffsetX > 0) { - (transformState.offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) - } else 0f - - val newOffsetY = if (maxOffsetY > 0) { - (transformState.offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) - } else 0f - - transformState.updateTransform(newScale, newOffsetX, newOffsetY) - } catch (_: Exception) { - transformState.resetToLast() - } - } - } - } - .onSizeChanged { size -> - onImageSizeChanged(Size(size.width.toFloat(), size.height.toFloat())) - } - ) -} - -/** - * 顶部工具栏组件 - */ -@Composable -private fun TopToolbar( - onDismiss: () -> Unit, - onFullscreen: () -> Unit, - onConfirm: () -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(24.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - // 关闭按钮 - ActionButton( - onClick = onDismiss, - icon = Icons.Default.Close, - contentDescription = stringResource(R.string.cancel), - backgroundColor = MaterialTheme.colorScheme.error.copy(alpha = 0.9f) - ) - - // 全屏按钮 - ActionButton( - onClick = onFullscreen, - icon = Icons.Default.Fullscreen, - contentDescription = stringResource(R.string.reprovision), - backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) - ) - - // 确认按钮 - ActionButton( - onClick = onConfirm, - icon = Icons.Default.Check, - contentDescription = stringResource(R.string.confirm), - backgroundColor = Color(0xFF4CAF50).copy(alpha = 0.9f) - ) - } -} - -/** - * 操作按钮组件 - */ -@Composable -private fun ActionButton( - onClick: () -> Unit, - icon: androidx.compose.ui.graphics.vector.ImageVector, - contentDescription: String, - backgroundColor: Color, - modifier: Modifier = Modifier -) { - var isPressed by remember { mutableStateOf(false) } - - val buttonScale by animateFloatAsState( - targetValue = if (isPressed) 0.85f else 1f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessHigh - ), - label = "ButtonScale" - ) - - val buttonAlpha by animateFloatAsState( - targetValue = if (isPressed) 0.8f else 1f, - animationSpec = tween(100), - label = "ButtonAlpha" - ) - - Surface( - onClick = { - isPressed = true - onClick() - }, - modifier = modifier - .size(64.dp) - .graphicsLayer( - scaleX = buttonScale, - scaleY = buttonScale, - alpha = buttonAlpha - ), - shape = CircleShape, - color = backgroundColor, - shadowElevation = 8.dp - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - tint = Color.White, - modifier = Modifier.size(28.dp) - ) - } - } - - LaunchedEffect(isPressed) { - if (isPressed) { - kotlinx.coroutines.delay(150) - isPressed = false - } - } -} - -/** - * 底部提示卡片组件 - */ -@Composable -private fun BottomHintCard( - modifier: Modifier = Modifier -) { - var isVisible by remember { mutableStateOf(true) } - - val cardAlpha by animateFloatAsState( - targetValue = if (isVisible) 1f else 0f, - animationSpec = tween( - durationMillis = 500, - easing = EaseInOutCubic - ), - label = "HintAlpha" - ) - - val cardTranslationY by animateFloatAsState( - targetValue = if (isVisible) 0f else 100f, - animationSpec = tween( - durationMillis = 500, - easing = EaseInOutCubic - ), - label = "HintTranslation" - ) - - LaunchedEffect(Unit) { - kotlinx.coroutines.delay(4000) - isVisible = false - } - - Card( - modifier = modifier - .fillMaxWidth() - .padding(24.dp) - .alpha(cardAlpha) - .graphicsLayer { - translationY = cardTranslationY - }, - colors = CardDefaults.cardColors( - containerColor = Color.Black.copy(alpha = 0.85f) - ), - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) - ) { - Text( - text = stringResource(id = R.string.image_editor_hint), - color = Color.White, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .padding(20.dp) - .fillMaxWidth() - ) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt deleted file mode 100644 index daf089b7..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.sukisu.ultra.ui.theme.util - -import android.content.ContentResolver -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.net.Uri -import android.util.Log -import androidx.core.graphics.createBitmap -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream - -data class BackgroundTransformation( - val scale: Float = 1f, - val offsetX: Float = 0f, - val offsetY: Float = 0f -) - -fun Context.getImageBitmap(uri: Uri): Bitmap? { - return try { - val contentResolver: ContentResolver = contentResolver - val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null - val bitmap = BitmapFactory.decodeStream(inputStream) - inputStream.close() - bitmap - } catch (e: Exception) { - Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}") - null - } -} - -fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap { - val width = bitmap.width - val height = bitmap.height - - // 创建与屏幕比例相同的目标位图 - val displayMetrics = resources.displayMetrics - val screenWidth = displayMetrics.widthPixels - val screenHeight = displayMetrics.heightPixels - val screenRatio = screenHeight.toFloat() / screenWidth.toFloat() - - // 计算目标宽高 - val targetWidth: Int - val targetHeight: Int - if (width.toFloat() / height.toFloat() > screenRatio) { - targetHeight = height - targetWidth = (height / screenRatio).toInt() - } else { - targetWidth = width - targetHeight = (width * screenRatio).toInt() - } - - // 创建与目标相同大小的位图 - val scaledBitmap = createBitmap(targetWidth, targetHeight) - val canvas = Canvas(scaledBitmap) - - val matrix = Matrix() - - // 确保缩放值有效 - val safeScale = maxOf(0.1f, transformation.scale) - matrix.postScale(safeScale, safeScale) - - // 计算偏移量,确保不会出现负最大值的问题 - val widthDiff = (bitmap.width * safeScale - targetWidth) - val heightDiff = (bitmap.height * safeScale - targetHeight) - - // 安全计算偏移量边界 - val maxOffsetX = maxOf(0f, widthDiff / 2) - val maxOffsetY = maxOf(0f, heightDiff / 2) - - // 限制偏移范围 - val safeOffsetX = if (maxOffsetX > 0) - transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f - val safeOffsetY = if (maxOffsetY > 0) - transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f - - // 应用偏移量到矩阵 - val translationX = -widthDiff / 2 + safeOffsetX - val translationY = -heightDiff / 2 + safeOffsetY - - matrix.postTranslate(translationX, translationY) - - // 将原始位图绘制到新位图上 - canvas.drawBitmap(bitmap, matrix, null) - - return scaledBitmap -} - -fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? { - try { - val bitmap = getImageBitmap(uri) ?: return null - val transformedBitmap = applyTransformationToBitmap(bitmap, transformation) - - val fileName = "custom_background_transformed.jpg" - val file = File(filesDir, fileName) - val outputStream = FileOutputStream(file) - - transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) - outputStream.flush() - outputStream.close() - - return Uri.fromFile(file) - } catch (e: Exception) { - Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e) - return null - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt deleted file mode 100644 index 1ba64d73..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sukisu.ultra.ui.util - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.compositionLocalOf - -val LocalSnackbarHost = compositionLocalOf { - error("CompositionLocal LocalSnackbarController not present") -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt index 035137fd..277f0fbc 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt @@ -7,23 +7,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri -import android.os.Build import android.os.Environment -import android.os.Handler -import android.os.Looper -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.core.content.ContextCompat -import androidx.core.net.toUri +import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.util.module.LatestVersionInfo -import java.io.File -import java.util.concurrent.TimeUnit - -private const val TAG = "DownloadUtil" -private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})" -private const val MAX_RETRY_COUNT = 3 -private const val RETRY_DELAY_MS = 3000L /** * @author weishu @@ -36,10 +25,8 @@ fun download( fileName: String, description: String, onDownloaded: (Uri) -> Unit = {}, - onDownloading: () -> Unit = {}, - onError: (String) -> Unit = {} + onDownloading: () -> Unit = {} ) { - Log.d(TAG, "Start Download: $url") val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val query = DownloadManager.Query() @@ -55,21 +42,14 @@ fun download( onDownloading() return } else if (status == DownloadManager.STATUS_SUCCESSFUL) { - onDownloaded(localUri.toUri()) + onDownloaded(Uri.parse(localUri)) return } } } } - val downloadFile = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - fileName - ) - if (downloadFile.exists()) { - downloadFile.delete() - } - val request = DownloadManager.Request(url.toUri()) + val request = DownloadManager.Request(Uri.parse(url)) .setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, fileName @@ -78,204 +58,48 @@ fun download( .setMimeType("application/zip") .setTitle(fileName) .setDescription(description) - .addRequestHeader("User-Agent", CUSTOM_USER_AGENT) - .setAllowedOverMetered(true) - .setAllowedOverRoaming(true) - .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) - try { - val downloadId = downloadManager.enqueue(request) - Log.d(TAG, "Successful launch of the download,ID: $downloadId") - monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError) - } catch (e: Exception) { - Log.e(TAG, "Download startup failure", e) - onError("Download startup failure: ${e.message}") - } -} - -private fun monitorDownload( - context: Context, - downloadManager: DownloadManager, - downloadId: Long, - url: String, - fileName: String, - description: String, - onDownloaded: (Uri) -> Unit, - onDownloading: () -> Unit, - onError: (String) -> Unit, - retryCount: Int = 0 -) { - val handler = Handler(Looper.getMainLooper()) - val query = DownloadManager.Query().setFilterById(downloadId) - - var lastProgress = -1 - var stuckCounter = 0 - - val runnable = object : Runnable { - override fun run() { - downloadManager.query(query).use { cursor -> - if (cursor != null && cursor.moveToFirst()) { - @SuppressLint("Range") - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - - when (status) { - DownloadManager.STATUS_SUCCESSFUL -> { - @SuppressLint("Range") - val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) - Log.d(TAG, "Download Successfully: $localUri") - onDownloaded(localUri.toUri()) - return - } - DownloadManager.STATUS_FAILED -> { - @SuppressLint("Range") - val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)) - Log.d(TAG, "Download failed with reason code: $reason") - - if (retryCount < MAX_RETRY_COUNT) { - Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}") - handler.postDelayed({ - downloadManager.remove(downloadId) - download(context, url, fileName, description, onDownloaded, onDownloading, onError) - }, RETRY_DELAY_MS) - } else { - onError("Download failed, please check network connection or storage space") - } - return - } - DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> { - @SuppressLint("Range") - val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) - @SuppressLint("Range") - val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) - - if (totalBytes > 0) { - val progress = (downloadedBytes * 100 / totalBytes).toInt() - if (progress == lastProgress) { - stuckCounter++ - if (stuckCounter > 30) { - if (retryCount < MAX_RETRY_COUNT) { - Log.d(TAG, "Download stalled and restarted") - downloadManager.remove(downloadId) - download(context, url, fileName, description, onDownloaded, onDownloading, onError) - return - } - } - } else { - lastProgress = progress - stuckCounter = 0 - Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)") - } - } - } - } - } - } - handler.postDelayed(this, 1000) - } - } - handler.post(runnable) - - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1 - if (id == downloadId) { - handler.removeCallbacks(runnable) - - val query = DownloadManager.Query().setFilterById(downloadId) - downloadManager.query(query).use { cursor -> - if (cursor != null && cursor.moveToFirst()) { - @SuppressLint("Range") - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - - if (status == DownloadManager.STATUS_SUCCESSFUL) { - @SuppressLint("Range") - val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) - onDownloaded(localUri.toUri()) - } else { - if (retryCount < MAX_RETRY_COUNT) { - download(context!!, url, fileName, description, onDownloaded, onDownloading, onError) - } else { - onError("Download failed, please try again later") - } - } - } - } - - context?.unregisterReceiver(this) - } - } - } - - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - ContextCompat.RECEIVER_EXPORTED - ) + downloadManager.enqueue(request) } fun checkNewVersion(): LatestVersionInfo { - val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest" + val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest" + // default null value if failed val defaultValue = LatestVersionInfo() - return runCatching { - val client = okhttp3.OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - - val request = okhttp3.Request.Builder() - .url(url) - .header("User-Agent", CUSTOM_USER_AGENT) - .build() - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - Log.d("CheckUpdate", "Network request failed: ${response.message}") - return defaultValue - } - val body = response.body?.string() - if (body == null) { - Log.d("CheckUpdate", "Return data is null") - return defaultValue - } - Log.d("CheckUpdate", "Return data: $body") - val json = org.json.JSONObject(body) - - // 直接从 tag_name 提取版本号(如 v1.1) - val tagName = json.optString("tag_name", "") - val versionName = tagName.removePrefix("v") // 移除前缀 "v" - - // 从 body 字段获取更新日志(保留换行符) - val changelog = json.optString("body") - .replace("\\r\\n", "\n") // 转换换行符 - - val assets = json.getJSONArray("assets") - for (i in 0 until assets.length()) { - val asset = assets.getJSONObject(i) - val name = asset.getString("name") - if (!name.endsWith(".apk")) continue - - val regex = Regex("SukiSU.*_(\\d+)-release") - val matchResult = regex.find(name) - if (matchResult == null) { - Log.d("CheckUpdate", "No matches found: $name, skip over") - continue + runCatching { + ksuApp.okhttpClient.newCall(okhttp3.Request.Builder().url(url).build()).execute() + .use { response -> + if (!response.isSuccessful) { + return defaultValue + } + val body = response.body?.string() ?: return defaultValue + val json = org.json.JSONObject(body) + val changelog = json.optString("body") + + val assets = json.getJSONArray("assets") + for (i in 0 until assets.length()) { + val asset = assets.getJSONObject(i) + val name = asset.getString("name") + if (!name.endsWith(".apk")) { + continue + } + + val regex = Regex("v(.+?)_(\\d+)-") + val matchResult = regex.find(name) ?: continue + val versionName = matchResult.groupValues[1] + val versionCode = matchResult.groupValues[2].toInt() + val downloadUrl = asset.getString("browser_download_url") + + return LatestVersionInfo( + versionCode, + downloadUrl, + changelog + ) } - val versionCode = matchResult.groupValues[1].toInt() - val downloadUrl = asset.getString("browser_download_url") - return LatestVersionInfo( - versionCode, - downloadUrl, - changelog, - versionName - ) } - Log.d("CheckUpdate", "No valid APK resource found, return default value") - defaultValue - } - }.getOrDefault(defaultValue) + } + return defaultValue } @Composable @@ -300,7 +124,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { val uri = cursor.getString( cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) ) - onDownloaded(uri.toUri()) + onDownloaded(Uri.parse(uri)) } } } @@ -316,4 +140,4 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { context.unregisterReceiver(receiver) } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java new file mode 100644 index 00000000..b7104115 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java @@ -0,0 +1,576 @@ +package com.sukisu.ultra.ui.util; +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.text.TextUtils; +import android.util.Log; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Locale; + +/** + * An object to convert Chinese character to its corresponding pinyin string. For characters with + * multiple possible pinyin string, only one is selected according to collator. Polyphone is not + * supported in this implementation. This class is implemented to achieve the best runtime + * performance and minimum runtime resources with tolerable sacrifice of accuracy. This + * implementation highly depends on zh_CN ICU collation data and must be always synchronized with + * ICU. + *

+ * Currently this file is aligned to zh.txt in ICU 4.6 + */ +public class HanziToPinyin { + private static final String TAG = "HanziToPinyin"; + + // Turn on this flag when we want to check internal data structure. + private static final boolean DEBUG = false; + + /** + * Unihans array. + *

+ * Each unihans is the first one within same pinyin when collator is zh_CN. + */ + public static final char[] UNIHANS = { + '\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b', + '\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954', + '\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43', + '\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2', + '\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe', + '\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284', + '\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403', + '\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb', + '\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306', + '\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413', + '\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a', + '\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201', + '\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be', + '\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6', + '\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a', + '\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11', + '\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b', + '\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe', + '\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52', + '\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f', + '\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677', + '\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf', + '\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0', + '\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755', + '\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b', + '\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c', + '\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938', + '\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269', + '\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b', + '\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f', + '\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6', + '\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1', + '\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264', + '\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa', + '\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c', + '\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149', + '\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041', + '\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f', + '\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974', + '\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4', + '\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478', + '\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15', + '\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03', + '\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2', + '\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a', + '\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba', + '\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c', + '\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2', + '\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7', + '\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962', + '\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce', + '\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01', + '\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf', + '\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc', + '\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254', + '\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077', + '\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75', + '\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1', + '\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61', + '\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11', + '\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079', + '\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94', + '\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0', + '\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142', + '\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897', + '\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577', + '\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9', + '\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd', + '\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72', + '\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a', + '\u6628', '\u5159', '\u9fc3', '\u9fc4',}; + + /** + * Pinyin array. + *

+ * Each pinyin is corresponding to unihans of same + * offset in the unihans array. + */ + public static final byte[][] PINYINS = { + {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, + {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, + {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, + {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, + {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, + {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, + {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, + {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, + {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, + {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, + {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, + {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, + {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, + {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, + {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, + {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, + {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, + {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, + {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, + {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, + {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, + {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, + {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, + {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, + {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, + {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, + {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, + {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, + {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, + {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, + {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, + {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, + {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, + {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, + {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, + {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, + {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, + {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, + {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, + {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, + {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, + {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, + {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, + {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, + {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, + {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, + {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, + {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, + {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, + {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, + {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, + {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, + {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, + {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, + {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, + {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, + {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, + {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, + {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, + {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, + {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, + {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, + {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, + {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, + {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, + {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, + {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, + {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, + {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, + {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, + {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, + {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, + {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, + {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, + {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, + {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, + {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, + {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, + {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, + {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, + {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, + {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, + {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, + {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, + {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, + {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, + {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, + {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, + {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, + {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, + {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, + {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, + {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, + {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, + {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, + {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, + {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, + {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, + {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, + {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, + {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, + {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, + {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, + {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, + {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, + {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, + {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, + {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, + {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, + {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, + {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, + {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, + {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, + {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, + {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, + {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, + {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, + {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, + {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, + {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, + {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, + {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, + {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, + {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, + {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, + {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, + {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, + {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, + {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, + {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, + {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, + {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, + {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, + {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, + {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, + {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, + {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, + {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, + {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, + {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, + {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, + {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, + {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, + {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, + {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, + {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, + {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, + {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, + {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, + {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, + {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, + {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, + {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, + {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, + {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, + {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, + {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, + {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, + {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, + {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, + {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, + {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, + {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, + {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, + {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, + {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, + {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, + {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, + {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, + {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, + {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, + {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, + {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, + {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, + {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, + {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, + {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, + {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, + {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, + {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, + {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, + {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, + {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, + {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, + {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, + {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, + {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, + {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, + {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, + {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, + {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, + {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, + {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, + {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, + {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, + {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, + {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, + {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, + {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, + {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, + {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, + {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, + {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, + {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, + {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, + {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, + {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, + {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, + {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, + {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, + {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, + {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},}; + + /** + * First and last Chinese character with known Pinyin according to zh collation + */ + private static final String FIRST_PINYIN_UNIHAN = "\u963F"; + private static final String LAST_PINYIN_UNIHAN = "\u9FFF"; + + private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); + + private static HanziToPinyin sInstance; + private final boolean mHasChinaCollator; + + public static class Token { + /** + * Separator between target string for each source char + */ + public static final String SEPARATOR = " "; + + public static final int LATIN = 1; + public static final int PINYIN = 2; + public static final int UNKNOWN = 3; + + public Token() { + } + + public Token(int type, String source, String target) { + this.type = type; + this.source = source; + this.target = target; + } + + /** + * Type of this token, ASCII, PINYIN or UNKNOWN. + */ + public int type; + /** + * Original string before translation. + */ + public String source; + /** + * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is + * original string in source. + */ + public String target; + } + + protected HanziToPinyin(boolean hasChinaCollator) { + mHasChinaCollator = hasChinaCollator; + } + + public static HanziToPinyin getInstance() { + synchronized (HanziToPinyin.class) { + if (sInstance != null) { + return sInstance; + } + // Check if zh_CN collation data is available + final Locale[] locale = Collator.getAvailableLocales(); + for (Locale value : locale) { + if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) { + // Do self validation just once. + if (DEBUG) { + Log.d(TAG, "Self validation. Result: " + doSelfValidation()); + } + sInstance = new HanziToPinyin(true); + return sInstance; + } + } + if (sInstance == null){//这个判断是用于处理国产ROM的兼容性问题 + if (Locale.CHINA.equals(Locale.getDefault())){ + sInstance = new HanziToPinyin(true); + return sInstance; + } + } + Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); + sInstance = new HanziToPinyin(false); + return sInstance; + } + } + + /** + * Validate if our internal table has some wrong value. + * + * @return true when the table looks correct. + */ + private static boolean doSelfValidation() { + char lastChar = UNIHANS[0]; + String lastString = Character.toString(lastChar); + for (char c : UNIHANS) { + if (lastChar == c) { + continue; + } + final String curString = Character.toString(c); + int cmp = COLLATOR.compare(lastString, curString); + if (cmp >= 0) { + Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString + + "\" is greater than current string \"" + curString + "\"."); + return false; + } + lastString = curString; + } + return true; + } + + private Token getToken(char character) { + Token token = new Token(); + final String letter = Character.toString(character); + token.source = letter; + int offset = -1; + int cmp; + if (character < 256) { + token.type = Token.LATIN; + token.target = letter; + return token; + } else { + cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); + if (cmp < 0) { + token.type = Token.UNKNOWN; + token.target = letter; + return token; + } else if (cmp == 0) { + token.type = Token.PINYIN; + offset = 0; + } else { + cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); + if (cmp > 0) { + token.type = Token.UNKNOWN; + token.target = letter; + return token; + } else if (cmp == 0) { + token.type = Token.PINYIN; + offset = UNIHANS.length - 1; + } + } + } + + token.type = Token.PINYIN; + if (offset < 0) { + int begin = 0; + int end = UNIHANS.length - 1; + while (begin <= end) { + offset = (begin + end) / 2; + final String unihan = Character.toString(UNIHANS[offset]); + cmp = COLLATOR.compare(letter, unihan); + if (cmp == 0) { + break; + } else if (cmp > 0) { + begin = offset + 1; + } else { + end = offset - 1; + } + } + } + if (cmp < 0) { + offset--; + } + StringBuilder pinyin = new StringBuilder(); + for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { + pinyin.append((char) PINYINS[offset][j]); + } + token.target = pinyin.toString(); + if (TextUtils.isEmpty(token.target)) { + token.type = Token.UNKNOWN; + token.target = token.source; + } + return token; + } + + /** + * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without + * space will be put into a Token, One Hanzi character which has pinyin will be treated as a + * Token. If these is no China collator, the empty token array is returned. + */ + public ArrayList get(final String input) { + ArrayList tokens = new ArrayList<>(); + if (!mHasChinaCollator || TextUtils.isEmpty(input)) { + // return empty tokens. + return tokens; + } + final int inputLength = input.length(); + final StringBuilder sb = new StringBuilder(); + int tokenType = Token.LATIN; + // Go through the input, create a new token when + // a. Token type changed + // b. Get the Pinyin of current charater. + // c. current character is space. + for (int i = 0; i < inputLength; i++) { + final char character = input.charAt(i); + if (character == ' ') { + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + } else if (character < 256) { + if (tokenType != Token.LATIN && sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + tokenType = Token.LATIN; + sb.append(character); + } else { + Token t = getToken(character); + if (t.type == Token.PINYIN) { + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + tokens.add(t); + tokenType = Token.PINYIN; + } else { + if (tokenType != t.type && sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + tokenType = t.type; + sb.append(character); + } + } + } + if (sb.length() > 0) { + addToken(sb, tokens, tokenType); + } + return tokens; + } + + private void addToken( + final StringBuilder sb, final ArrayList tokens, final int tokenType) { + String str = sb.toString(); + tokens.add(new Token(tokenType, str, str)); + sb.setLength(0); + } + + public String toPinyinString(String string) { + if (string == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + ArrayList tokens = get(string); + for (Token token : tokens) { + sb.append(token.target); + } + return sb.toString().toLowerCase(); + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt deleted file mode 100644 index edfbdf52..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt +++ /dev/null @@ -1,522 +0,0 @@ -package com.sukisu.ultra.ui.util - -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.text.TextUtils -import android.util.Log -import java.text.Collator -import java.util.Locale - -class HanziToPinyin private constructor(val hasChinaCollator: Boolean) { - - class Token( - var type: Int = 0, - var source: String = "", - var target: String = "" - ) { - companion object { - const val LATIN = 1 - const val PINYIN = 2 - const val UNKNOWN = 3 - } - } - - private fun getToken(character: Char): Token { - val token = Token() - val letter = character.toString() - token.source = letter - var offset = -1 - var cmp: Int - - if (character < 256.toChar()) { - token.type = Token.LATIN - token.target = letter - return token - } else { - cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN) - if (cmp < 0) { - token.type = Token.UNKNOWN - token.target = letter - return token - } else if (cmp == 0) { - token.type = Token.PINYIN - offset = 0 - } else { - cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN) - if (cmp > 0) { - token.type = Token.UNKNOWN - token.target = letter - return token - } else if (cmp == 0) { - token.type = Token.PINYIN - offset = UNIHANS.size - 1 - } - } - } - - token.type = Token.PINYIN - if (offset < 0) { - var begin = 0 - var end = UNIHANS.size - 1 - while (begin <= end) { - offset = (begin + end) / 2 - val unihan = UNIHANS[offset].toString() - cmp = COLLATOR.compare(letter, unihan) - when { - cmp == 0 -> break - cmp > 0 -> begin = offset + 1 - else -> end = offset - 1 - } - } - } - if (cmp < 0) { - offset-- - } - - val pinyin = StringBuilder() - for (j in PINYINS[offset].indices) { - if (PINYINS[offset][j] == 0.toByte()) break - pinyin.append(PINYINS[offset][j].toInt().toChar()) - } - token.target = pinyin.toString() - if (TextUtils.isEmpty(token.target)) { - token.type = Token.UNKNOWN - token.target = token.source - } - return token - } - - fun get(input: String?): ArrayList { - val tokens = ArrayList() - if (!hasChinaCollator || TextUtils.isEmpty(input)) { - return tokens - } - - val inputLength = input!!.length - val sb = StringBuilder() - var tokenType = Token.LATIN - - for (i in 0 until inputLength) { - val character = input[i] - when { - character == ' ' -> { - if (sb.isNotEmpty()) { - addToken(sb, tokens, tokenType) - } - } - character < 256.toChar() -> { - if (tokenType != Token.LATIN && sb.isNotEmpty()) { - addToken(sb, tokens, tokenType) - } - tokenType = Token.LATIN - sb.append(character) - } - else -> { - val t = getToken(character) - if (t.type == Token.PINYIN) { - if (sb.isNotEmpty()) { - addToken(sb, tokens, tokenType) - } - tokens.add(t) - tokenType = Token.PINYIN - } else { - if (tokenType != t.type && sb.isNotEmpty()) { - addToken(sb, tokens, tokenType) - } - tokenType = t.type - sb.append(character) - } - } - } - } - if (sb.isNotEmpty()) { - addToken(sb, tokens, tokenType) - } - return tokens - } - - private fun addToken(sb: StringBuilder, tokens: ArrayList, tokenType: Int) { - val str = sb.toString() - tokens.add(Token(tokenType, str, str)) - sb.setLength(0) - } - - fun toPinyinString(string: String?): String? { - if (string == null) { - return null - } - val sb = StringBuilder() - val tokens = get(string) - for (token in tokens) { - sb.append(token.target) - } - return sb.toString().lowercase() - } - - companion object { - private const val TAG = "HanziToPinyin" - private const val DEBUG = false - - val UNIHANS = charArrayOf( - '阿', '哎', '安', '肮', '凹', '八', - '挀', '扳', '邦', '勹', '陂', '奔', - '伻', '屄', '边', '灬', '憋', '汃', - '冫', '癶', '峬', '嚓', '偲', '参', - '仓', '撡', '冊', '嵾', '曽', '曾', - '層', '叉', '芆', '辿', '伥', '抄', - '车', '抻', '沈', '沉', '阷', '吃', - '充', '抽', '出', '欻', '揣', '巛', - '刅', '吹', '旾', '逴', '呲', '匆', - '凑', '粗', '汆', '崔', '邨', '搓', - '咑', '呆', '丹', '当', '刀', '嘚', - '扥', '灯', '氐', '嗲', '甸', '刁', - '爹', '丁', '丟', '东', '吺', '厾', - '耑', '襨', '吨', '多', '妸', '诶', - '奀', '鞥', '儿', '发', '帆', '匚', - '飞', '分', '丰', '覅', '仏', '紑', - '伕', '旮', '侅', '甘', '冈', '皋', - '戈', '给', '根', '刯', '工', '勾', - '估', '瓜', '乖', '关', '光', '归', - '丨', '呙', '哈', '咍', '佄', '夯', - '茠', '诃', '黒', '拫', '亨', '噷', - '叿', '齁', '乯', '花', '怀', '犿', - '巟', '灰', '昏', '吙', '丌', '加', - '戋', '江', '艽', '阶', '巾', '坕', - '冂', '丩', '凥', '姢', '噘', '军', - '咔', '开', '刊', '忼', '尻', '匼', - '肎', '劥', '空', '抠', '扝', '夸', - '蒯', '宽', '匡', '亏', '坤', '扩', - '垃', '来', '兰', '啷', '捞', '肋', - '勒', '崚', '刕', '俩', '奁', '良', - '撩', '列', '拎', '刢', '溜', '囖', - '龙', '瞜', '噜', '娈', '畧', '抡', - '罗', '呣', '妈', '埋', '嫚', '牤', - '猫', '么', '呅', '门', '甿', '咪', - '宀', '喵', '乜', '民', '名', '谬', - '摸', '哞', '毪', '嗯', '拏', '腉', - '囡', '囔', '孬', '疒', '娞', '恁', - '能', '妮', '拈', '嬢', '鸟', '捏', - '囜', '宁', '妞', '农', '羺', '奴', - '奻', '疟', '黁', '郍', '喔', '讴', - '妑', '拍', '眅', '乓', '抛', '呸', - '喷', '匉', '丕', '囨', '剽', '氕', - '姘', '乒', '钋', '剖', '仆', '七', - '掐', '千', '呛', '悄', '癿', '亲', - '狅', '芎', '丘', '区', '峑', '缺', - '夋', '呥', '穣', '娆', '惹', '人', - '扔', '日', '茸', '厹', '邚', '挼', - '堧', '婑', '瞤', '捼', '仨', '毢', - '三', '桒', '掻', '閪', '森', '僧', - '杀', '筛', '山', '伤', '弰', '奢', - '申', '莘', '敒', '升', '尸', '収', - '书', '刷', '衰', '闩', '双', '谁', - '吮', '说', '厶', '忪', '捜', '苏', - '狻', '夊', '孙', '唆', '他', '囼', - '坍', '汤', '夲', '忑', '熥', '剔', - '天', '旫', '帖', '厅', '囲', '偷', - '凸', '湍', '推', '吞', '乇', '穵', - '歪', '弯', '尣', '危', '昷', '翁', - '挝', '乌', '夕', '虲', '仚', '乡', - '灱', '些', '心', '星', '凶', '休', - '吁', '吅', '削', '坃', '丫', '恹', - '央', '幺', '倻', '一', '囙', '应', - '哟', '佣', '优', '扜', '囦', '曰', - '晕', '筠', '筼', '帀', '災', '兂', - '匨', '傮', '则', '贼', '怎', '増', - '扎', '捚', '沾', '张', '长', '長', - '佋', '蜇', '贞', '争', '之', '峙', - '庢', '中', '州', '朱', '抓', '拽', - '专', '妆', '隹', '宒', '卓', '乲', - '宗', '邹', '租', '钻', '厜', '尊', - '昨', '兙', '鿃', '鿄' - ) - - val PINYINS = arrayOf( - byteArrayOf(65, 0, 0, 0, 0, 0), byteArrayOf(65, 73, 0, 0, 0, 0), - byteArrayOf(65, 78, 0, 0, 0, 0), byteArrayOf(65, 78, 71, 0, 0, 0), - byteArrayOf(65, 79, 0, 0, 0, 0), byteArrayOf(66, 65, 0, 0, 0, 0), - byteArrayOf(66, 65, 73, 0, 0, 0), byteArrayOf(66, 65, 78, 0, 0, 0), - byteArrayOf(66, 65, 78, 71, 0, 0), byteArrayOf(66, 65, 79, 0, 0, 0), - byteArrayOf(66, 69, 73, 0, 0, 0), byteArrayOf(66, 69, 78, 0, 0, 0), - byteArrayOf(66, 69, 78, 71, 0, 0), byteArrayOf(66, 73, 0, 0, 0, 0), - byteArrayOf(66, 73, 65, 78, 0, 0), byteArrayOf(66, 73, 65, 79, 0, 0), - byteArrayOf(66, 73, 69, 0, 0, 0), byteArrayOf(66, 73, 78, 0, 0, 0), - byteArrayOf(66, 73, 78, 71, 0, 0), byteArrayOf(66, 79, 0, 0, 0, 0), - byteArrayOf(66, 85, 0, 0, 0, 0), byteArrayOf(67, 65, 0, 0, 0, 0), - byteArrayOf(67, 65, 73, 0, 0, 0), byteArrayOf(67, 65, 78, 0, 0, 0), - byteArrayOf(67, 65, 78, 71, 0, 0), byteArrayOf(67, 65, 79, 0, 0, 0), - byteArrayOf(67, 69, 0, 0, 0, 0), byteArrayOf(67, 69, 78, 0, 0, 0), - byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), - byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(67, 72, 65, 0, 0, 0), - byteArrayOf(67, 72, 65, 73, 0, 0), byteArrayOf(67, 72, 65, 78, 0, 0), - byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(67, 72, 65, 79, 0, 0), - byteArrayOf(67, 72, 69, 0, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), - byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), - byteArrayOf(67, 72, 69, 78, 71, 0), byteArrayOf(67, 72, 73, 0, 0, 0), - byteArrayOf(67, 72, 79, 78, 71, 0), byteArrayOf(67, 72, 79, 85, 0, 0), - byteArrayOf(67, 72, 85, 0, 0, 0), byteArrayOf(67, 72, 85, 65, 0, 0), - byteArrayOf(67, 72, 85, 65, 73, 0), byteArrayOf(67, 72, 85, 65, 78, 0), - byteArrayOf(67, 72, 85, 65, 78, 71), byteArrayOf(67, 72, 85, 73, 0, 0), - byteArrayOf(67, 72, 85, 78, 0, 0), byteArrayOf(67, 72, 85, 79, 0, 0), - byteArrayOf(67, 73, 0, 0, 0, 0), byteArrayOf(67, 79, 78, 71, 0, 0), - byteArrayOf(67, 79, 85, 0, 0, 0), byteArrayOf(67, 85, 0, 0, 0, 0), - byteArrayOf(67, 85, 65, 78, 0, 0), byteArrayOf(67, 85, 73, 0, 0, 0), - byteArrayOf(67, 85, 78, 0, 0, 0), byteArrayOf(67, 85, 79, 0, 0, 0), - byteArrayOf(68, 65, 0, 0, 0, 0), byteArrayOf(68, 65, 73, 0, 0, 0), - byteArrayOf(68, 65, 78, 0, 0, 0), byteArrayOf(68, 65, 78, 71, 0, 0), - byteArrayOf(68, 65, 79, 0, 0, 0), byteArrayOf(68, 69, 0, 0, 0, 0), - byteArrayOf(68, 69, 78, 0, 0, 0), byteArrayOf(68, 69, 78, 71, 0, 0), - byteArrayOf(68, 73, 0, 0, 0, 0), byteArrayOf(68, 73, 65, 0, 0, 0), - byteArrayOf(68, 73, 65, 78, 0, 0), byteArrayOf(68, 73, 65, 79, 0, 0), - byteArrayOf(68, 73, 69, 0, 0, 0), byteArrayOf(68, 73, 78, 71, 0, 0), - byteArrayOf(68, 73, 85, 0, 0, 0), byteArrayOf(68, 79, 78, 71, 0, 0), - byteArrayOf(68, 79, 85, 0, 0, 0), byteArrayOf(68, 85, 0, 0, 0, 0), - byteArrayOf(68, 85, 65, 78, 0, 0), byteArrayOf(68, 85, 73, 0, 0, 0), - byteArrayOf(68, 85, 78, 0, 0, 0), byteArrayOf(68, 85, 79, 0, 0, 0), - byteArrayOf(69, 0, 0, 0, 0, 0), byteArrayOf(69, 73, 0, 0, 0, 0), - byteArrayOf(69, 78, 0, 0, 0, 0), byteArrayOf(69, 78, 71, 0, 0, 0), - byteArrayOf(69, 82, 0, 0, 0, 0), byteArrayOf(70, 65, 0, 0, 0, 0), - byteArrayOf(70, 65, 78, 0, 0, 0), byteArrayOf(70, 65, 78, 71, 0, 0), - byteArrayOf(70, 69, 73, 0, 0, 0), byteArrayOf(70, 69, 78, 0, 0, 0), - byteArrayOf(70, 69, 78, 71, 0, 0), byteArrayOf(70, 73, 65, 79, 0, 0), - byteArrayOf(70, 79, 0, 0, 0, 0), byteArrayOf(70, 79, 85, 0, 0, 0), - byteArrayOf(70, 85, 0, 0, 0, 0), byteArrayOf(71, 65, 0, 0, 0, 0), - byteArrayOf(71, 65, 73, 0, 0, 0), byteArrayOf(71, 65, 78, 0, 0, 0), - byteArrayOf(71, 65, 78, 71, 0, 0), byteArrayOf(71, 65, 79, 0, 0, 0), - byteArrayOf(71, 69, 0, 0, 0, 0), byteArrayOf(71, 69, 73, 0, 0, 0), - byteArrayOf(71, 69, 78, 0, 0, 0), byteArrayOf(71, 69, 78, 71, 0, 0), - byteArrayOf(71, 79, 78, 71, 0, 0), byteArrayOf(71, 79, 85, 0, 0, 0), - byteArrayOf(71, 85, 0, 0, 0, 0), byteArrayOf(71, 85, 65, 0, 0, 0), - byteArrayOf(71, 85, 65, 73, 0, 0), byteArrayOf(71, 85, 65, 78, 0, 0), - byteArrayOf(71, 85, 65, 78, 71, 0), byteArrayOf(71, 85, 73, 0, 0, 0), - byteArrayOf(71, 85, 78, 0, 0, 0), byteArrayOf(71, 85, 79, 0, 0, 0), - byteArrayOf(72, 65, 0, 0, 0, 0), byteArrayOf(72, 65, 73, 0, 0, 0), - byteArrayOf(72, 65, 78, 0, 0, 0), byteArrayOf(72, 65, 78, 71, 0, 0), - byteArrayOf(72, 65, 79, 0, 0, 0), byteArrayOf(72, 69, 0, 0, 0, 0), - byteArrayOf(72, 69, 73, 0, 0, 0), byteArrayOf(72, 69, 78, 0, 0, 0), - byteArrayOf(72, 69, 78, 71, 0, 0), byteArrayOf(72, 77, 0, 0, 0, 0), - byteArrayOf(72, 79, 78, 71, 0, 0), byteArrayOf(72, 79, 85, 0, 0, 0), - byteArrayOf(72, 85, 0, 0, 0, 0), byteArrayOf(72, 85, 65, 0, 0, 0), - byteArrayOf(72, 85, 65, 73, 0, 0), byteArrayOf(72, 85, 65, 78, 0, 0), - byteArrayOf(72, 85, 65, 78, 71, 0), byteArrayOf(72, 85, 73, 0, 0, 0), - byteArrayOf(72, 85, 78, 0, 0, 0), byteArrayOf(72, 85, 79, 0, 0, 0), - byteArrayOf(74, 73, 0, 0, 0, 0), byteArrayOf(74, 73, 65, 0, 0, 0), - byteArrayOf(74, 73, 65, 78, 0, 0), byteArrayOf(74, 73, 65, 78, 71, 0), - byteArrayOf(74, 73, 65, 79, 0, 0), byteArrayOf(74, 73, 69, 0, 0, 0), - byteArrayOf(74, 73, 78, 0, 0, 0), byteArrayOf(74, 73, 78, 71, 0, 0), - byteArrayOf(74, 73, 79, 78, 71, 0), byteArrayOf(74, 73, 85, 0, 0, 0), - byteArrayOf(74, 85, 0, 0, 0, 0), byteArrayOf(74, 85, 65, 78, 0, 0), - byteArrayOf(74, 85, 69, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), - byteArrayOf(75, 65, 0, 0, 0, 0), byteArrayOf(75, 65, 73, 0, 0, 0), - byteArrayOf(75, 65, 78, 0, 0, 0), byteArrayOf(75, 65, 78, 71, 0, 0), - byteArrayOf(75, 65, 79, 0, 0, 0), byteArrayOf(75, 69, 0, 0, 0, 0), - byteArrayOf(75, 69, 78, 0, 0, 0), byteArrayOf(75, 69, 78, 71, 0, 0), - byteArrayOf(75, 79, 78, 71, 0, 0), byteArrayOf(75, 79, 85, 0, 0, 0), - byteArrayOf(75, 85, 0, 0, 0, 0), byteArrayOf(75, 85, 65, 0, 0, 0), - byteArrayOf(75, 85, 65, 73, 0, 0), byteArrayOf(75, 85, 65, 78, 0, 0), - byteArrayOf(75, 85, 65, 78, 71, 0), byteArrayOf(75, 85, 73, 0, 0, 0), - byteArrayOf(75, 85, 78, 0, 0, 0), byteArrayOf(75, 85, 79, 0, 0, 0), - byteArrayOf(76, 65, 0, 0, 0, 0), byteArrayOf(76, 65, 73, 0, 0, 0), - byteArrayOf(76, 65, 78, 0, 0, 0), byteArrayOf(76, 65, 78, 71, 0, 0), - byteArrayOf(76, 65, 79, 0, 0, 0), byteArrayOf(76, 69, 0, 0, 0, 0), - byteArrayOf(76, 69, 73, 0, 0, 0), byteArrayOf(76, 69, 78, 71, 0, 0), - byteArrayOf(76, 73, 0, 0, 0, 0), byteArrayOf(76, 73, 65, 0, 0, 0), - byteArrayOf(76, 73, 65, 78, 0, 0), byteArrayOf(76, 73, 65, 78, 71, 0), - byteArrayOf(76, 73, 65, 79, 0, 0), byteArrayOf(76, 73, 69, 0, 0, 0), - byteArrayOf(76, 73, 78, 0, 0, 0), byteArrayOf(76, 73, 78, 71, 0, 0), - byteArrayOf(76, 73, 85, 0, 0, 0), byteArrayOf(76, 79, 0, 0, 0, 0), - byteArrayOf(76, 79, 78, 71, 0, 0), byteArrayOf(76, 79, 85, 0, 0, 0), - byteArrayOf(76, 85, 0, 0, 0, 0), byteArrayOf(76, 85, 65, 78, 0, 0), - byteArrayOf(76, 85, 69, 0, 0, 0), byteArrayOf(76, 85, 78, 0, 0, 0), - byteArrayOf(76, 85, 79, 0, 0, 0), byteArrayOf(77, 0, 0, 0, 0, 0), - byteArrayOf(77, 65, 0, 0, 0, 0), byteArrayOf(77, 65, 73, 0, 0, 0), - byteArrayOf(77, 65, 78, 0, 0, 0), byteArrayOf(77, 65, 78, 71, 0, 0), - byteArrayOf(77, 65, 79, 0, 0, 0), byteArrayOf(77, 69, 0, 0, 0, 0), - byteArrayOf(77, 69, 73, 0, 0, 0), byteArrayOf(77, 69, 78, 0, 0, 0), - byteArrayOf(77, 69, 78, 71, 0, 0), byteArrayOf(77, 73, 0, 0, 0, 0), - byteArrayOf(77, 73, 65, 78, 0, 0), byteArrayOf(77, 73, 65, 79, 0, 0), - byteArrayOf(77, 73, 69, 0, 0, 0), byteArrayOf(77, 73, 78, 0, 0, 0), - byteArrayOf(77, 73, 78, 71, 0, 0), byteArrayOf(77, 73, 85, 0, 0, 0), - byteArrayOf(77, 79, 0, 0, 0, 0), byteArrayOf(77, 79, 85, 0, 0, 0), - byteArrayOf(77, 85, 0, 0, 0, 0), byteArrayOf(78, 0, 0, 0, 0, 0), - byteArrayOf(78, 65, 0, 0, 0, 0), byteArrayOf(78, 65, 73, 0, 0, 0), - byteArrayOf(78, 65, 78, 0, 0, 0), byteArrayOf(78, 65, 78, 71, 0, 0), - byteArrayOf(78, 65, 79, 0, 0, 0), byteArrayOf(78, 69, 0, 0, 0, 0), - byteArrayOf(78, 69, 73, 0, 0, 0), byteArrayOf(78, 69, 78, 0, 0, 0), - byteArrayOf(78, 69, 78, 71, 0, 0), byteArrayOf(78, 73, 0, 0, 0, 0), - byteArrayOf(78, 73, 65, 78, 0, 0), byteArrayOf(78, 73, 65, 78, 71, 0), - byteArrayOf(78, 73, 65, 79, 0, 0), byteArrayOf(78, 73, 69, 0, 0, 0), - byteArrayOf(78, 73, 78, 0, 0, 0), byteArrayOf(78, 73, 78, 71, 0, 0), - byteArrayOf(78, 73, 85, 0, 0, 0), byteArrayOf(78, 79, 78, 71, 0, 0), - byteArrayOf(78, 79, 85, 0, 0, 0), byteArrayOf(78, 85, 0, 0, 0, 0), - byteArrayOf(78, 85, 65, 78, 0, 0), byteArrayOf(78, 85, 69, 0, 0, 0), - byteArrayOf(78, 85, 78, 0, 0, 0), byteArrayOf(78, 85, 79, 0, 0, 0), - byteArrayOf(79, 0, 0, 0, 0, 0), byteArrayOf(79, 85, 0, 0, 0, 0), - byteArrayOf(80, 65, 0, 0, 0, 0), byteArrayOf(80, 65, 73, 0, 0, 0), - byteArrayOf(80, 65, 78, 0, 0, 0), byteArrayOf(80, 65, 78, 71, 0, 0), - byteArrayOf(80, 65, 79, 0, 0, 0), byteArrayOf(80, 69, 73, 0, 0, 0), - byteArrayOf(80, 69, 78, 0, 0, 0), byteArrayOf(80, 69, 78, 71, 0, 0), - byteArrayOf(80, 73, 0, 0, 0, 0), byteArrayOf(80, 73, 65, 78, 0, 0), - byteArrayOf(80, 73, 65, 79, 0, 0), byteArrayOf(80, 73, 69, 0, 0, 0), - byteArrayOf(80, 73, 78, 0, 0, 0), byteArrayOf(80, 73, 78, 71, 0, 0), - byteArrayOf(80, 79, 0, 0, 0, 0), byteArrayOf(80, 79, 85, 0, 0, 0), - byteArrayOf(80, 85, 0, 0, 0, 0), byteArrayOf(81, 73, 0, 0, 0, 0), - byteArrayOf(81, 73, 65, 0, 0, 0), byteArrayOf(81, 73, 65, 78, 0, 0), - byteArrayOf(81, 73, 65, 78, 71, 0), byteArrayOf(81, 73, 65, 79, 0, 0), - byteArrayOf(81, 73, 69, 0, 0, 0), byteArrayOf(81, 73, 78, 0, 0, 0), - byteArrayOf(81, 73, 78, 71, 0, 0), byteArrayOf(81, 73, 79, 78, 71, 0), - byteArrayOf(81, 73, 85, 0, 0, 0), byteArrayOf(81, 85, 0, 0, 0, 0), - byteArrayOf(81, 85, 65, 78, 0, 0), byteArrayOf(81, 85, 69, 0, 0, 0), - byteArrayOf(81, 85, 78, 0, 0, 0), byteArrayOf(82, 65, 78, 0, 0, 0), - byteArrayOf(82, 65, 78, 71, 0, 0), byteArrayOf(82, 65, 79, 0, 0, 0), - byteArrayOf(82, 69, 0, 0, 0, 0), byteArrayOf(82, 69, 78, 0, 0, 0), - byteArrayOf(82, 69, 78, 71, 0, 0), byteArrayOf(82, 73, 0, 0, 0, 0), - byteArrayOf(82, 79, 78, 71, 0, 0), byteArrayOf(82, 79, 85, 0, 0, 0), - byteArrayOf(82, 85, 0, 0, 0, 0), byteArrayOf(82, 85, 65, 0, 0, 0), - byteArrayOf(82, 85, 65, 78, 0, 0), byteArrayOf(82, 85, 73, 0, 0, 0), - byteArrayOf(82, 85, 78, 0, 0, 0), byteArrayOf(82, 85, 79, 0, 0, 0), - byteArrayOf(83, 65, 0, 0, 0, 0), byteArrayOf(83, 65, 73, 0, 0, 0), - byteArrayOf(83, 65, 78, 0, 0, 0), byteArrayOf(83, 65, 78, 71, 0, 0), - byteArrayOf(83, 65, 79, 0, 0, 0), byteArrayOf(83, 69, 0, 0, 0, 0), - byteArrayOf(83, 69, 78, 0, 0, 0), byteArrayOf(83, 69, 78, 71, 0, 0), - byteArrayOf(83, 72, 65, 0, 0, 0), byteArrayOf(83, 72, 65, 73, 0, 0), - byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(83, 72, 65, 78, 71, 0), - byteArrayOf(83, 72, 65, 79, 0, 0), byteArrayOf(83, 72, 69, 0, 0, 0), - byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(88, 73, 78, 0, 0, 0), - byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(83, 72, 69, 78, 71, 0), - byteArrayOf(83, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 79, 85, 0, 0), - byteArrayOf(83, 72, 85, 0, 0, 0), byteArrayOf(83, 72, 85, 65, 0, 0), - byteArrayOf(83, 72, 85, 65, 73, 0), byteArrayOf(83, 72, 85, 65, 78, 0), - byteArrayOf(83, 72, 85, 65, 78, 71), byteArrayOf(83, 72, 85, 73, 0, 0), - byteArrayOf(83, 72, 85, 78, 0, 0), byteArrayOf(83, 72, 85, 79, 0, 0), - byteArrayOf(83, 73, 0, 0, 0, 0), byteArrayOf(83, 79, 78, 71, 0, 0), - byteArrayOf(83, 79, 85, 0, 0, 0), byteArrayOf(83, 85, 0, 0, 0, 0), - byteArrayOf(83, 85, 65, 78, 0, 0), byteArrayOf(83, 85, 73, 0, 0, 0), - byteArrayOf(83, 85, 78, 0, 0, 0), byteArrayOf(83, 85, 79, 0, 0, 0), - byteArrayOf(84, 65, 0, 0, 0, 0), byteArrayOf(84, 65, 73, 0, 0, 0), - byteArrayOf(84, 65, 78, 0, 0, 0), byteArrayOf(84, 65, 78, 71, 0, 0), - byteArrayOf(84, 65, 79, 0, 0, 0), byteArrayOf(84, 69, 0, 0, 0, 0), - byteArrayOf(84, 69, 78, 71, 0, 0), byteArrayOf(84, 73, 0, 0, 0, 0), - byteArrayOf(84, 73, 65, 78, 0, 0), byteArrayOf(84, 73, 65, 79, 0, 0), - byteArrayOf(84, 73, 69, 0, 0, 0), byteArrayOf(84, 73, 78, 71, 0, 0), - byteArrayOf(84, 79, 78, 71, 0, 0), byteArrayOf(84, 79, 85, 0, 0, 0), - byteArrayOf(84, 85, 0, 0, 0, 0), byteArrayOf(84, 85, 65, 78, 0, 0), - byteArrayOf(84, 85, 73, 0, 0, 0), byteArrayOf(84, 85, 78, 0, 0, 0), - byteArrayOf(84, 85, 79, 0, 0, 0), byteArrayOf(87, 65, 0, 0, 0, 0), - byteArrayOf(87, 65, 73, 0, 0, 0), byteArrayOf(87, 65, 78, 0, 0, 0), - byteArrayOf(87, 65, 78, 71, 0, 0), byteArrayOf(87, 69, 73, 0, 0, 0), - byteArrayOf(87, 69, 78, 0, 0, 0), byteArrayOf(87, 69, 78, 71, 0, 0), - byteArrayOf(87, 79, 0, 0, 0, 0), byteArrayOf(87, 85, 0, 0, 0, 0), - byteArrayOf(88, 73, 0, 0, 0, 0), byteArrayOf(88, 73, 65, 0, 0, 0), - byteArrayOf(88, 73, 65, 78, 0, 0), byteArrayOf(88, 73, 65, 78, 71, 0), - byteArrayOf(88, 73, 65, 79, 0, 0), byteArrayOf(88, 73, 69, 0, 0, 0), - byteArrayOf(88, 73, 78, 0, 0, 0), byteArrayOf(88, 73, 78, 71, 0, 0), - byteArrayOf(88, 73, 79, 78, 71, 0), byteArrayOf(88, 73, 85, 0, 0, 0), - byteArrayOf(88, 85, 0, 0, 0, 0), byteArrayOf(88, 85, 65, 78, 0, 0), - byteArrayOf(88, 85, 69, 0, 0, 0), byteArrayOf(88, 85, 78, 0, 0, 0), - byteArrayOf(89, 65, 0, 0, 0, 0), byteArrayOf(89, 65, 78, 0, 0, 0), - byteArrayOf(89, 65, 78, 71, 0, 0), byteArrayOf(89, 65, 79, 0, 0, 0), - byteArrayOf(89, 69, 0, 0, 0, 0), byteArrayOf(89, 73, 0, 0, 0, 0), - byteArrayOf(89, 73, 78, 0, 0, 0), byteArrayOf(89, 73, 78, 71, 0, 0), - byteArrayOf(89, 79, 0, 0, 0, 0), byteArrayOf(89, 79, 78, 71, 0, 0), - byteArrayOf(89, 79, 85, 0, 0, 0), byteArrayOf(89, 85, 0, 0, 0, 0), - byteArrayOf(89, 85, 65, 78, 0, 0), byteArrayOf(89, 85, 69, 0, 0, 0), - byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), - byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(90, 65, 0, 0, 0, 0), - byteArrayOf(90, 65, 73, 0, 0, 0), byteArrayOf(90, 65, 78, 0, 0, 0), - byteArrayOf(90, 65, 78, 71, 0, 0), byteArrayOf(90, 65, 79, 0, 0, 0), - byteArrayOf(90, 69, 0, 0, 0, 0), byteArrayOf(90, 69, 73, 0, 0, 0), - byteArrayOf(90, 69, 78, 0, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), - byteArrayOf(90, 72, 65, 0, 0, 0), byteArrayOf(90, 72, 65, 73, 0, 0), - byteArrayOf(90, 72, 65, 78, 0, 0), byteArrayOf(90, 72, 65, 78, 71, 0), - byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(90, 72, 65, 78, 71, 0), - byteArrayOf(90, 72, 65, 79, 0, 0), byteArrayOf(90, 72, 69, 0, 0, 0), - byteArrayOf(90, 72, 69, 78, 0, 0), byteArrayOf(90, 72, 69, 78, 71, 0), - byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 73, 0, 0, 0), - byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(90, 72, 79, 78, 71, 0), - byteArrayOf(90, 72, 79, 85, 0, 0), byteArrayOf(90, 72, 85, 0, 0, 0), - byteArrayOf(90, 72, 85, 65, 0, 0), byteArrayOf(90, 72, 85, 65, 73, 0), - byteArrayOf(90, 72, 85, 65, 78, 0), byteArrayOf(90, 72, 85, 65, 78, 71), - byteArrayOf(90, 72, 85, 73, 0, 0), byteArrayOf(90, 72, 85, 78, 0, 0), - byteArrayOf(90, 72, 85, 79, 0, 0), byteArrayOf(90, 73, 0, 0, 0, 0), - byteArrayOf(90, 79, 78, 71, 0, 0), byteArrayOf(90, 79, 85, 0, 0, 0), - byteArrayOf(90, 85, 0, 0, 0, 0), byteArrayOf(90, 85, 65, 78, 0, 0), - byteArrayOf(90, 85, 73, 0, 0, 0), byteArrayOf(90, 85, 78, 0, 0, 0), - byteArrayOf(90, 85, 79, 0, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0), - byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0) - ) - - private const val FIRST_PINYIN_UNIHAN = "阿" - private const val LAST_PINYIN_UNIHAN = "鿿" - - private val COLLATOR: Collator = Collator.getInstance(Locale.CHINA) - - private var sInstance: HanziToPinyin? = null - - fun getInstance(): HanziToPinyin { - synchronized(HanziToPinyin::class.java) { - if (sInstance != null) { - return sInstance!! - } - - val locale = Collator.getAvailableLocales() - for (value in locale) { - if (value == Locale.CHINA || value.language.contains("zh")) { - if (DEBUG) { - Log.d(TAG, "Self validation. Result: ${doSelfValidation()}") - } - sInstance = HanziToPinyin(true) - return sInstance!! - } - } - - if (sInstance == null) { - if (Locale.CHINA == Locale.getDefault()) { - sInstance = HanziToPinyin(true) - return sInstance!! - } - } - - Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled") - sInstance = HanziToPinyin(false) - return sInstance!! - } - } - - private fun doSelfValidation(): Boolean { - val lastChar = UNIHANS[0] - var lastString = lastChar.toString() - for (c in UNIHANS) { - if (lastChar == c) { - continue - } - val curString = c.toString() - val cmp = COLLATOR.compare(lastString, curString) - if (cmp >= 0) { - Log.e( - TAG, - "Internal error in Unihan table. The last string \"$lastString\" " + - "is greater than current string \"$curString\"." - ) - return false - } - lastString = curString - } - return true - } - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt deleted file mode 100644 index 36ea19c3..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.sukisu.ultra.ui.util - -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import java.util.regex.Pattern - -@Composable -fun LinkifyText( - text: String, - modifier: Modifier = Modifier -) { - val uriHandler = LocalUriHandler.current - val layoutResult = remember { - mutableStateOf(null) - } - val linksList = extractUrls(text) - val annotatedString = buildAnnotatedString { - append(text) - linksList.forEach { - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - start = it.start, - end = it.end - ) - addStringAnnotation( - tag = "URL", - annotation = it.url, - start = it.start, - end = it.end - ) - } - } - Text( - text = annotatedString, - modifier = modifier.pointerInput(Unit) { - detectTapGestures { offsetPosition -> - layoutResult.value?.let { - val position = it.getOffsetForPosition(offsetPosition) - annotatedString.getStringAnnotations(position, position).firstOrNull() - ?.let { result -> - if (result.tag == "URL") { - uriHandler.openUri(result.item) - } - } - } - } - }, - onTextLayout = { layoutResult.value = it } - ) -} - -private val urlPattern: Pattern = Pattern.compile( - "(?:^|\\W)((ht|f)tp(s?)://|www\\.)" - + "(([\\w\\-]+\\.)+([\\w\\-.~]+/?)*" - + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]*$~@!:/{};']*)", - Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL -) - -private data class LinkInfo( - val url: String, - val start: Int, - val end: Int -) - -@Suppress("HttpUrlsUsage") -private fun extractUrls(text: String): List = buildList { - val matcher = urlPattern.matcher(text) - while (matcher.find()) { - val matchStart = matcher.start(1) - val matchEnd = matcher.end() - val url = text.substring(matchStart, matchEnd).replaceFirst("http://", "https://") - add(LinkInfo(url, matchStart, matchEnd)) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt index c05cb26c..4492ff83 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt @@ -33,8 +33,13 @@ private fun getKsuDaemonPath(): String { return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so" } +data class FlashResult(val code: Int, val err: String, val showReboot: Boolean) { + constructor(result: Shell.Result, showReboot: Boolean) : this(result.code, result.err.joinToString("\n"), showReboot) + constructor(result: Shell.Result) : this(result, result.isSuccess) +} + object KsuCli { - var SHELL: Shell = createRootShell() + val SHELL: Shell = createRootShell() val GLOBAL_MNT_SHELL: Shell = createRootShell(true) } @@ -135,6 +140,13 @@ fun toggleModule(id: String, enable: Boolean): Boolean { return result } +fun undoUninstallModule(id: String): Boolean { + val cmd = "module undo-uninstall $id" + val result = execKsud(cmd, true) + Log.i(TAG, "undo uninstall module $id result: $result") + return result +} + fun uninstallModule(id: String): Boolean { val cmd = "module uninstall $id" val result = execKsud(cmd, true) @@ -142,13 +154,6 @@ fun uninstallModule(id: String): Boolean { return result } -fun restoreModule(id: String): Boolean { - val cmd = "module restore $id" - val result = execKsud(cmd, true) - Log.i(TAG, "restore module $id result: $result") - return result -} - private fun flashWithIO( cmd: String, onStdout: (String) -> Unit, @@ -174,10 +179,9 @@ private fun flashWithIO( fun flashModule( uri: Uri, - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit -): Boolean { +): FlashResult { val resolver = ksuApp.contentResolver with(resolver.openInputStream(uri)) { val file = File(ksuApp.cacheDir, "module.zip") @@ -190,8 +194,7 @@ fun flashModule( file.delete() - onFinish(result.isSuccess, result.code) - return result.isSuccess + return FlashResult(result) } } @@ -220,26 +223,19 @@ fun runModuleAction( } fun restoreBoot( - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit -): Boolean { + onStdout: (String) -> Unit, onStderr: (String) -> Unit +): FlashResult { val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") - val result = flashWithIO( - "${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", - onStdout, - onStderr - ) - onFinish(result.isSuccess, result.code) - return result.isSuccess + val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr) + return FlashResult(result) } fun uninstallPermanently( - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit -): Boolean { + onStdout: (String) -> Unit, onStderr: (String) -> Unit +): FlashResult { val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") - val result = - flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) - onFinish(result.isSuccess, result.code) - return result.isSuccess + val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) + return FlashResult(result) } @Parcelize @@ -254,10 +250,9 @@ fun installBoot( lkm: LkmSelection, ota: Boolean, partition: String?, - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit, -): Boolean { +): FlashResult { val resolver = ksuApp.contentResolver val bootFile = bootUri?.let { uri -> @@ -324,13 +319,11 @@ fun installBoot( lkmFile?.delete() // if boot uri is empty, it is direct install, when success, we should show reboot button - onFinish(bootUri == null && result.isSuccess, result.code) - - if (bootUri == null && result.isSuccess) { - install() + val showReboot = bootUri == null && result.isSuccess // we create a temporary val here, to avoid calc showReboot double + if (showReboot) { // because we decide do not update ksud when startActivity + install() // install ksud here } - - return result.isSuccess + return FlashResult(result, showReboot) } fun reboot(reason: String = "") { @@ -347,7 +340,6 @@ fun rootAvailable(): Boolean { return shell.isRoot } - suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = "boot-info current-kmi" @@ -394,6 +386,12 @@ suspend fun getAvailablePartitions(): List = withContext(Dispatchers.IO) out.filter { it.isNotBlank() }.map { it.trim() } } +fun overlayFsAvailable(): Boolean { + val shell = getRootShell() + // check /proc/filesystems + return ShellUtils.fastCmdResult(shell, "cat /proc/filesystems | grep overlay") +} + fun hasMagisk(): Boolean { val shell = getRootShell(true) val result = shell.newJob().add("which magisk").exec() @@ -454,69 +452,6 @@ fun deleteAppProfileTemplate(id: String): Boolean { return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") .to(ArrayList(), null).exec().isSuccess } -// KPM控制 -fun loadKpmModule(path: String, args: String? = null): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm load $path ${args ?: ""}" - return ShellUtils.fastCmd(shell, cmd) -} - -fun unloadKpmModule(name: String): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm unload $name" - return ShellUtils.fastCmd(shell, cmd) -} - -fun getKpmModuleCount(): Int { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm num" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim().toIntOrNull() ?: 0 -} - -fun runCmd(shell: Shell, cmd: String): String { - return shell.newJob() - .add(cmd) - .to(mutableListOf(), null) - .exec().out - .joinToString("\n") -} - -fun listKpmModules(): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm list" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to list KPM modules", e) - "" - } -} - -fun getKpmModuleInfo(name: String): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm info $name" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to get KPM module info: $name", e) - "" - } -} - -fun controlKpmModule(name: String, args: String? = null): Int { - val shell = getRootShell() - val cmd = """${getKsuDaemonPath()} kpm control $name "${args ?: ""}"""" - val result = runCmd(shell, cmd) - return result.trim().toIntOrNull() ?: -1 -} - -fun getKpmVersion(): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} kpm version" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() -} fun forceStopApp(packageName: String) { val shell = getRootShell() @@ -525,7 +460,6 @@ fun forceStopApp(packageName: String) { } fun launchApp(packageName: String) { - val shell = getRootShell() val result = shell.newJob() @@ -538,187 +472,3 @@ fun restartApp(packageName: String) { forceStopApp(packageName) launchApp(packageName) } - -fun getSuSFSDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksu_susfs.so" -} - -fun getSuSFSVersion(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show version") - return result -} - -fun getSuSFSVariant(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show variant") - return result -} - -fun getSuSFSFeatures(): String { - val shell = getRootShell() - val cmd = "${getSuSFSDaemonPath()} show enabled_features" - return runCmd(shell, cmd) -} - -fun getZygiskImplement(): String { - val shell = getRootShell() - - val zygiskModuleIds = listOf( - "zygisksu", - "rezygisk", - "shirokozygisk" - ) - - for (moduleId in zygiskModuleIds) { - val modulePath = "/data/adb/modules/$moduleId" - when { - ShellUtils.fastCmdResult(shell, "test -f $modulePath/module.prop && test ! -f $modulePath/disable") -> { - val result = ShellUtils.fastCmd(shell, "grep '^name=' $modulePath/module.prop | cut -d'=' -f2") - Log.i(TAG, "Zygisk implement: $result") - return result - } - } - } - - Log.i(TAG, "Zygisk implement: None") - return "None" -} - -fun getUidScannerDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libuid_scanner.so" -} - -private const val targetPath = "/data/adb/uid_scanner" -fun ensureUidScannerExecutable(): Boolean { - val shell = getRootShell() - val uidScannerPath = getUidScannerDaemonPath() - if (!ShellUtils.fastCmdResult(shell, "test -f $targetPath")) { - val copyResult = ShellUtils.fastCmdResult(shell, "cp $uidScannerPath $targetPath") - if (!copyResult) { - return false - } - } - - val result = ShellUtils.fastCmdResult(shell, "chmod 755 $targetPath") - return result -} - -fun setUidAutoScan(enabled: Boolean): Boolean { - val shell = getRootShell() - if (!ensureUidScannerExecutable()) { - return false - } - - val enableValue = if (enabled) 1 else 0 - val cmd = "$targetPath --auto-scan $enableValue && $targetPath reload" - val result = ShellUtils.fastCmdResult(shell, cmd) - - val throneResult = Natives.setUidScannerEnabled(enabled) - - return result && throneResult -} - -fun setUidMultiUserScan(enabled: Boolean): Boolean { - val shell = getRootShell() - if (!ensureUidScannerExecutable()) { - return false - } - - val enableValue = if (enabled) 1 else 0 - val cmd = "$targetPath --multi-user $enableValue && $targetPath reload" - val result = ShellUtils.fastCmdResult(shell, cmd) - return result -} - -fun getUidMultiUserScan(): Boolean { - val shell = getRootShell() - - val cmd = "grep 'multi_user_scan=' /data/misc/user_uid/uid_scanner.conf | cut -d'=' -f2" - val result = ShellUtils.fastCmd(shell, cmd).trim() - - return try { - result.toInt() == 1 - } catch (_: NumberFormatException) { - false - } -} - -fun cleanRuntimeEnvironment(): Boolean { - val shell = getRootShell() - return try { - try { - ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop") - } catch (_: Exception) { - } - ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh") - Natives.clearUidScannerEnvironment() - true - } catch (_: Exception) { - false - } -} - -fun readUidScannerFile(): Boolean { - val shell = getRootShell() - return try { - ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1" - } catch (_: Exception) { - false - } -} - -fun addUmountPath(path: String, flags: Int): Boolean { - val shell = getRootShell() - val flagsArg = if (flags >= 0) "--flags $flags" else "" - val cmd = "${getKsuDaemonPath()} umount add $path $flagsArg" - val result = ShellUtils.fastCmdResult(shell, cmd) - Log.i(TAG, "add umount path $path result: $result") - return result -} - -fun removeUmountPath(path: String): Boolean { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} umount remove $path" - val result = ShellUtils.fastCmdResult(shell, cmd) - Log.i(TAG, "remove umount path $path result: $result") - return result -} - -fun listUmountPaths(): String { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} umount list" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to list umount paths", e) - "" - } -} - -fun clearCustomUmountPaths(): Boolean { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} umount clear-custom" - val result = ShellUtils.fastCmdResult(shell, cmd) - Log.i(TAG, "clear custom umount paths result: $result") - return result -} - -fun saveUmountConfig(): Boolean { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} umount save" - val result = ShellUtils.fastCmdResult(shell, cmd) - Log.i(TAG, "save umount config result: $result") - return result -} - -fun applyUmountConfigToKernel(): Boolean { - val shell = getRootShell() - val cmd = "${getKsuDaemonPath()} umount apply" - val result = ShellUtils.fastCmdResult(shell, cmd) - Log.i(TAG, "apply umount config to kernel result: $result") - return result -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt index b7e5216f..3034af61 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt @@ -1,19 +1,34 @@ package com.sukisu.ultra.ui.util -import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.topjohnwu.superuser.Shell import com.sukisu.ultra.R -import com.topjohnwu.superuser.io.SuFile -fun getSELinuxStatus(context: Context) = SuFile("/sys/fs/selinux/enforce").run { - when { - !exists() -> context.getString(R.string.selinux_status_disabled) - !isFile -> context.getString(R.string.selinux_status_unknown) - !canRead() -> context.getString(R.string.selinux_status_enforcing) - else -> when (runCatching { newInputStream() }.getOrNull()?.bufferedReader() - ?.use { it.runCatching { readLine() }.getOrNull()?.trim()?.toIntOrNull() }) { - 1 -> context.getString(R.string.selinux_status_enforcing) - 0 -> context.getString(R.string.selinux_status_permissive) - else -> context.getString(R.string.selinux_status_unknown) +@Composable +fun getSELinuxStatus(): String { + val shell = Shell.Builder.create() + .setFlags(Shell.FLAG_REDIRECT_STDERR) + .build("sh") + + val list = ArrayList() + val result = shell.use { + it.newJob().add("getenforce").to(list, list).exec() + } + val output = result.out.joinToString("\n").trim() + + if (result.isSuccess) { + return when (output) { + "Enforcing" -> stringResource(R.string.selinux_status_enforcing) + "Permissive" -> stringResource(R.string.selinux_status_permissive) + "Disabled" -> stringResource(R.string.selinux_status_disabled) + else -> stringResource(R.string.selinux_status_unknown) } } -} \ No newline at end of file + + return if (output.endsWith("Permission denied")) { + stringResource(R.string.selinux_status_enforcing) + } else { + stringResource(R.string.selinux_status_unknown) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/UidGroupUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/UidGroupUtils.kt new file mode 100644 index 00000000..dbd87bf2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/UidGroupUtils.kt @@ -0,0 +1,56 @@ +package com.sukisu.ultra.ui.util + +import com.sukisu.ultra.Natives +import com.sukisu.ultra.ksuApp +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel + +private val PREFERRED_PKG_BY_SUID = mapOf( + "android.uid.system" to "android", + "android.uid.phone" to "com.android.phone", + "android.uid.bluetooth" to "com.android.bluetooth", + "android.uid.nfc" to "com.android.nfc", +) + +fun pickPrimary(apps: List): SuperUserViewModel.AppInfo { + if (apps.isEmpty()) throw IllegalArgumentException("apps must not be empty") + val labeled = apps.filter { it.packageInfo.sharedUserLabel != 0 } + if (labeled.isNotEmpty()) { + return labeled.minWith(compareBy({ it.packageName.length }, { it.packageName })) + } + val bySuid = apps.groupBy { it.packageInfo.sharedUserId ?: "" } + .filterKeys { it.startsWith("android.uid.") } + if (bySuid.isEmpty()) return apps.first() + val suid = bySuid.keys.minOf { it } + val group = bySuid[suid] ?: apps + val preferredPkg = PREFERRED_PKG_BY_SUID[suid] + preferredPkg?.let { pkg -> + group.firstOrNull { it.packageName == pkg }?.let { return it } + } + return group.minWith(compareBy({ it.packageName.length }, { it.packageName })) +} + +val ownerNameCache = mutableMapOf() +fun ownerNameForUid(uid: Int): String { + ownerNameCache[uid]?.let { return it.ifEmpty { uid.toString() } } + val apps = SuperUserViewModel.apps.filter { it.uid == uid } + val labeledApp = apps.firstOrNull { it.packageInfo.sharedUserLabel != 0 } + val name = if (labeledApp != null) { + val pm = ksuApp.packageManager + val resId = labeledApp.packageInfo.sharedUserLabel + val text = runCatching { pm.getText(labeledApp.packageName, resId, labeledApp.packageInfo.applicationInfo) }.getOrNull() + text?.toString() ?: "" + } else { + Natives.getUserName(uid) ?: "" + } + val appId = uid % 100000 + val isAppRange = appId in 10000..19999 + val isUA = name.matches(Regex("u\\d+_a\\d+")) + val sharedUserId = apps.firstOrNull { !it.packageInfo.sharedUserId.isNullOrEmpty() }?.packageInfo?.sharedUserId + val finalName = if (isAppRange && isUA && !sharedUserId.isNullOrEmpty()) { + sharedUserId + } else { + name + } + ownerNameCache[uid] = finalName + return finalName.ifEmpty { uid.toString() } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt index 6c134a50..eb7a3758 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt @@ -1,8 +1,7 @@ package com.sukisu.ultra.ui.util.module data class LatestVersionInfo( - val versionCode : Int = 0, - val downloadUrl : String = "", - val changelog : String = "", - val versionName: String = "" + val versionCode: Int = 0, + val downloadUrl: String = "", + val changelog: String = "" ) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt deleted file mode 100644 index c0f52b1f..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt +++ /dev/null @@ -1,457 +0,0 @@ -package com.sukisu.ultra.ui.util.module - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.reboot -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.text.SimpleDateFormat -import java.util.* - -object ModuleModify { - @Composable - fun RestoreConfirmationDialog( - showDialog: Boolean, - onConfirm: () -> Unit, - onDismiss: () -> Unit - ) { - val context = LocalContext.current - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = context.getString(R.string.restore_confirm_title), - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - Text( - text = context.getString(R.string.restore_confirm_message), - style = MaterialTheme.typography.bodyMedium - ) - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(context.getString(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(context.getString(R.string.cancel)) - } - } - ) - } - } - - @Composable - fun AllowlistRestoreConfirmationDialog( - showDialog: Boolean, - onConfirm: () -> Unit, - onDismiss: () -> Unit - ) { - val context = LocalContext.current - - if (showDialog) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = context.getString(R.string.allowlist_restore_confirm_title), - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - Text( - text = context.getString(R.string.allowlist_restore_confirm_message), - style = MaterialTheme.typography.bodyMedium - ) - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(context.getString(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(context.getString(R.string.cancel)) - } - } - ) - } - } - - suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - withContext(Dispatchers.IO) { - try { - val busyboxPath = "/data/adb/ksu/bin/busybox" - val moduleDir = "/data/adb/modules" - - // 直接将tar输出重定向到用户选择的文件 - val command = """ - cd "$moduleDir" && - $busyboxPath tar -cz ./* > /proc/self/fd/1 - """.trimIndent() - - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command)) - - // 直接将tar输出写入到用户选择的文件 - context.contentResolver.openOutputStream(uri)?.use { output -> - process.inputStream.copyTo(output) - } - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.backup_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e("Backup", context.getString(R.string.backup_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.backup_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun restoreModules( - context: Context, - snackBarHost: SnackbarHostState, - uri: Uri, - showConfirmDialog: (Boolean) -> Unit, - confirmResult: CompletableDeferred - ) { - // 显示确认对话框 - withContext(Dispatchers.Main) { - showConfirmDialog(true) - } - - val userConfirmed = confirmResult.await() - if (!userConfirmed) return - - withContext(Dispatchers.IO) { - try { - val busyboxPath = "/data/adb/ksu/bin/busybox" - val moduleDir = "/data/adb/modules" - - // 直接从用户选择的文件读取并解压 - val process = Runtime.getRuntime() - .exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) - - context.contentResolver.openInputStream(uri)?.use { input -> - input.copyTo(process.outputStream) - } - process.outputStream.close() - - process.waitFor() - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - val snackbarResult = snackBarHost.showSnackbar( - message = context.getString(R.string.restore_success), - actionLabel = context.getString(R.string.restart_now), - duration = SnackbarDuration.Long - ) - if (snackbarResult == SnackbarResult.ActionPerformed) { - reboot() - } - } - - } catch (e: Exception) { - Log.e("Restore", context.getString(R.string.restore_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - message = context.getString( - R.string.restore_failed, - e.message ?: context.getString(R.string.unknown_error) - ), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - withContext(Dispatchers.IO) { - try { - val allowlistPath = "/data/adb/ksu/.allowlist" - - // 直接复制文件到用户选择的位置 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "cat $allowlistPath")) - - context.contentResolver.openOutputStream(uri)?.use { output -> - process.inputStream.copyTo(output) - } - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_backup_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e("AllowlistBackup", context.getString(R.string.allowlist_backup_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_backup_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun restoreAllowlist( - context: Context, - snackBarHost: SnackbarHostState, - uri: Uri, - showConfirmDialog: (Boolean) -> Unit, - confirmResult: CompletableDeferred - ) { - // 显示确认对话框 - withContext(Dispatchers.Main) { - showConfirmDialog(true) - } - - val userConfirmed = confirmResult.await() - if (!userConfirmed) return - - withContext(Dispatchers.IO) { - try { - val allowlistPath = "/data/adb/ksu/.allowlist" - - // 直接从用户选择的文件读取并写入到目标位置 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "cat > $allowlistPath")) - - context.contentResolver.openInputStream(uri)?.use { input -> - input.copyTo(process.outputStream) - } - process.outputStream.close() - - process.waitFor() - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_restore_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e( - "AllowlistRestore", - context.getString(R.string.allowlist_restore_failed, ""), - e - ) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_restore_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - @Composable - fun rememberModuleBackupLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - backupModules(context, snackBarHost, uri) - } - } - } - } - - @Composable - fun rememberModuleRestoreLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: CoroutineScope = rememberCoroutineScope() - ): ActivityResultLauncher { - var showRestoreDialog by remember { mutableStateOf(false) } - var restoreConfirmResult by remember { mutableStateOf?>(null) } - - // 显示恢复确认对话框 - RestoreConfirmationDialog( - showDialog = showRestoreDialog, - onConfirm = { - showRestoreDialog = false - restoreConfirmResult?.complete(true) - }, - onDismiss = { - showRestoreDialog = false - restoreConfirmResult?.complete(false) - } - ) - - return rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - val confirmResult = CompletableDeferred() - restoreConfirmResult = confirmResult - - restoreModules( - context = context, - snackBarHost = snackBarHost, - uri = uri, - showConfirmDialog = { show -> showRestoreDialog = show }, - confirmResult = confirmResult - ) - } - } - } - } - } - - @Composable - fun rememberAllowlistBackupLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - backupAllowlist(context, snackBarHost, uri) - } - } - } - } - - @Composable - fun rememberAllowlistRestoreLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: CoroutineScope = rememberCoroutineScope() - ): ActivityResultLauncher { - var showAllowlistRestoreDialog by remember { mutableStateOf(false) } - var allowlistRestoreConfirmResult by remember { - mutableStateOf?>( - null - ) - } - - // 显示允许列表恢复确认对话框 - AllowlistRestoreConfirmationDialog( - showDialog = showAllowlistRestoreDialog, - onConfirm = { - showAllowlistRestoreDialog = false - allowlistRestoreConfirmResult?.complete(true) - }, - onDismiss = { - showAllowlistRestoreDialog = false - allowlistRestoreConfirmResult?.complete(false) - } - ) - - return rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - val confirmResult = CompletableDeferred() - allowlistRestoreConfirmResult = confirmResult - - restoreAllowlist( - context = context, - snackBarHost = snackBarHost, - uri = uri, - showConfirmDialog = { show -> showAllowlistRestoreDialog = show }, - confirmResult = confirmResult - ) - } - } - } - } - } - - fun createBackupIntent(): Intent { - return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/zip" - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - putExtra(Intent.EXTRA_TITLE, "modules_backup_$timestamp.zip") - } - } - - fun createRestoreIntent(): Intent { - return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/zip" - } - } - - fun createAllowlistBackupIntent(): Intent { - return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/octet-stream" - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - putExtra(Intent.EXTRA_TITLE, "ksu_allowlist_backup_$timestamp.dat") - } - } - - fun createAllowlistRestoreIntent(): Intent { - return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/octet-stream" - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt deleted file mode 100644 index 5113b4ac..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.sukisu.ultra.ui.util.module - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.util.Log -import com.sukisu.ultra.R -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.nio.charset.StandardCharsets -import java.util.zip.ZipInputStream - -object ModuleUtils { - private const val TAG = "ModuleUtils" - - fun extractModuleName(context: Context, uri: Uri): String { - if (uri == Uri.EMPTY) { - Log.e(TAG, "The supplied URI is empty") - return context.getString(R.string.unknown_module) - } - - return try { - Log.d(TAG, "Start extracting module names from URIs: $uri") - - // 从URI路径中提取文件名 - val fileName = uri.lastPathSegment?.let { path -> - val lastSlash = path.lastIndexOf('/') - if (lastSlash != -1 && lastSlash < path.length - 1) { - path.substring(lastSlash + 1) - } else { - path - } - }?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module) - - val formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim() - var moduleName = formattedFileName - - try { - // 打开ZIP文件输入流 - val inputStream = context.contentResolver.openInputStream(uri) - if (inputStream == null) { - Log.e(TAG, "Unable to get input stream from URI: $uri") - return formattedFileName - } - - val zipInputStream = ZipInputStream(inputStream) - var entry = zipInputStream.nextEntry - - // 遍历ZIP文件中的条目,查找module.prop文件 - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - moduleName = line.substringAfter("=") - moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim() - break - } - } - break - } - entry = zipInputStream.nextEntry - } - zipInputStream.close() - Log.d(TAG, "Successfully extracted module name: $moduleName") - moduleName - } catch (e: IOException) { - Log.e(TAG, "Error reading ZIP file: ${e.message}") - formattedFileName - } - } catch (e: Exception) { - Log.e(TAG, "Exception when extracting module name: ${e.message}") - context.getString(R.string.unknown_module) - } - } - - // 验证URI是否有效并可访问 - fun isUriAccessible(context: Context, uri: Uri): Boolean { - if (uri == Uri.EMPTY) return false - - return try { - val inputStream = context.contentResolver.openInputStream(uri) - inputStream?.close() - inputStream != null - } catch (e: Exception) { - Log.e(TAG, "The URI is inaccessible: $uri, Error: ${e.message}") - false - } - } - - // 获取URI的持久权限 - fun takePersistableUriPermission(context: Context, uri: Uri) { - try { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, flags) - Log.d(TAG, "Persistent permissions for URIs have been obtained: $uri") - } catch (e: Exception) { - Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}") - } - } - - fun extractModuleId(context: Context, uri: Uri): String? { - if (uri == Uri.EMPTY) { - return null - } - - return try { - - val inputStream = context.contentResolver.openInputStream(uri) ?: return null - - val zipInputStream = ZipInputStream(inputStream) - var entry = zipInputStream.nextEntry - var moduleId: String? = null - - // 遍历ZIP文件中的条目,查找module.prop文件 - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("id=") == true) { - moduleId = line.substringAfter("=").trim() - break - } - } - break - } - entry = zipInputStream.nextEntry - } - zipInputStream.close() - moduleId - } catch (e: Exception) { - Log.e(TAG, "提取模块ID时发生异常: ${e.message}", e) - null - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt deleted file mode 100644 index 41948295..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt +++ /dev/null @@ -1,233 +0,0 @@ -package com.sukisu.ultra.ui.util.module - -import android.content.Context -import android.net.Uri -import android.util.Log -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.util.getRootShell -import java.io.File -import java.io.FileOutputStream - -/** - * @author ShirkNeko - * @date 2025/8/3 - */ - -// 模块签名验证工具类 -object ModuleSignatureUtils { - private const val TAG = "ModuleSignatureUtils" - - fun verifyModuleSignature(context: Context, moduleUri: Uri): Boolean { - return try { - // 创建临时文件 - val tempFile = File(context.cacheDir, "temp_module_${System.currentTimeMillis()}.zip") - - // 复制URI内容到临时文件 - context.contentResolver.openInputStream(moduleUri)?.use { inputStream -> - FileOutputStream(tempFile).use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - // 调用native方法验证签名 - val isVerified = Natives.verifyModuleSignature(tempFile.absolutePath) - - // 清理临时文件 - tempFile.delete() - - Log.d(TAG, "Module signature verification result: $isVerified") - isVerified - } catch (e: Exception) { - Log.e(TAG, "Error verifying module signature", e) - false - } - } - -} - -// 验证模块签名 -fun verifyModuleSignature(context: Context, moduleUri: Uri): Boolean { - return ModuleSignatureUtils.verifyModuleSignature(context, moduleUri) -} - -object ModuleOperationUtils { - private const val TAG = "ModuleOperationUtils" - - fun handleModuleInstallSuccess(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) { - if (!isSignatureVerified) { - Log.d(TAG, "模块签名未验证,跳过创建验证标志") - return - } - - try { - // 从ZIP文件提取模块ID - val moduleId = ModuleUtils.extractModuleId(context, moduleUri) - if (moduleId == null) { - Log.e(TAG, "无法提取模块ID,无法创建验证标志") - return - } - - // 创建验证标志文件 - val success = ModuleVerificationManager.createVerificationFlag(moduleId) - if (success) { - Log.d(TAG, "模块 $moduleId 验证标志创建成功") - } else { - Log.e(TAG, "模块 $moduleId 验证标志创建失败") - } - } catch (e: Exception) { - Log.e(TAG, "处理模块安装成功时发生异常", e) - } - } - - fun handleModuleUninstall(moduleId: String) { - try { - val success = ModuleVerificationManager.removeVerificationFlag(moduleId) - if (success) { - Log.d(TAG, "模块 $moduleId 验证标志移除成功") - } else { - Log.d(TAG, "模块 $moduleId 验证标志移除失败或不存在") - } - } catch (e: Exception) { - Log.e(TAG, "处理模块卸载时发生异常: $moduleId", e) - } - } - fun handleModuleUpdate(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) { - try { - val moduleId = ModuleUtils.extractModuleId(context, moduleUri) - if (moduleId == null) { - Log.e(TAG, "无法提取模块ID,无法处理验证标志") - return - } - - if (isSignatureVerified) { - // 签名验证通过,创建或更新验证标志 - val success = ModuleVerificationManager.createVerificationFlag(moduleId) - if (success) { - Log.d(TAG, "模块 $moduleId 更新后验证标志已更新") - } else { - Log.e(TAG, "模块 $moduleId 更新后验证标志更新失败") - } - } else { - // 签名验证失败,移除验证标志 - ModuleVerificationManager.removeVerificationFlag(moduleId) - Log.d(TAG, "模块 $moduleId 更新后签名未验证,验证标志已移除") - } - } catch (e: Exception) { - Log.e(TAG, "处理模块更新时发生异常", e) - } - } -} - -object ModuleVerificationManager { - private const val TAG = "ModuleVerificationManager" - private const val VERIFICATION_FLAGS_DIR = "/data/adb/ksu/verified_modules" - - // 为指定模块创建验证标志文件 - fun createVerificationFlag(moduleId: String): Boolean { - return try { - val shell = getRootShell() - val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" - - // 确保目录存在 - val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'" - shell.newJob().add(createDirCommand).exec() - - // 创建验证标志文件,写入验证时间戳 - val timestamp = System.currentTimeMillis() - val command = "echo '$timestamp' > '$flagFilePath'" - - val result = shell.newJob().add(command).exec() - - if (result.isSuccess) { - Log.d(TAG, "验证标志文件创建成功: $flagFilePath") - true - } else { - Log.e(TAG, "验证标志文件创建失败: $moduleId") - false - } - } catch (e: Exception) { - Log.e(TAG, "创建验证标志文件时发生异常: $moduleId", e) - false - } - } - - fun removeVerificationFlag(moduleId: String): Boolean { - return try { - val shell = getRootShell() - val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" - - val command = "rm -f '$flagFilePath'" - val result = shell.newJob().add(command).exec() - - if (result.isSuccess) { - Log.d(TAG, "验证标志文件移除成功: $flagFilePath") - true - } else { - Log.e(TAG, "验证标志文件移除失败: $moduleId") - false - } - } catch (e: Exception) { - Log.e(TAG, "移除验证标志文件时发生异常: $moduleId", e) - false - } - } - - fun getVerificationTimestamp(moduleId: String): Long { - return try { - val shell = getRootShell() - val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId" - - val command = "cat '$flagFilePath' 2>/dev/null || echo '0'" - val result = shell.newJob().add(command).to(ArrayList(), null).exec() - - if (result.isSuccess && result.out.isNotEmpty()) { - val timestampStr = result.out.firstOrNull()?.trim() ?: "0" - timestampStr.toLongOrNull() ?: 0L - } else { - 0L - } - } catch (e: Exception) { - Log.e(TAG, "获取验证时间戳时发生异常: $moduleId", e) - 0L - } - } - - fun batchCheckVerificationStatus(moduleIds: List): Map { - if (moduleIds.isEmpty()) return emptyMap() - - return try { - val shell = getRootShell() - val result = mutableMapOf() - - // 确保目录存在 - val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'" - shell.newJob().add(createDirCommand).exec() - - // 批量检查所有模块的验证标志文件 - val commands = moduleIds.map { moduleId -> - "test -f '$VERIFICATION_FLAGS_DIR/$moduleId' && echo '$moduleId:true' || echo '$moduleId:false'" - } - - val command = commands.joinToString(" && ") - val shellResult = shell.newJob().add(command).to(ArrayList(), null).exec() - - if (shellResult.isSuccess) { - shellResult.out.forEach { line -> - val parts = line.split(":") - if (parts.size == 2) { - val moduleId = parts[0] - val isVerified = parts[1] == "true" - result[moduleId] = isVerified - } - } - } - - Log.d(TAG, "批量验证检查完成,共检查 ${moduleIds.size} 个模块") - result - } catch (e: Exception) { - Log.e(TAG, "批量检查验证状态时发生异常", e) - // 返回默认值,所有模块都标记为未验证 - moduleIds.associateWith { false } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt deleted file mode 100644 index 72e069f9..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt +++ /dev/null @@ -1,590 +0,0 @@ -package com.sukisu.ultra.ui.viewmodel - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.system.Os -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.sukisu.ultra.KernelVersion -import com.sukisu.ultra.Natives -import com.sukisu.ultra.getKernelVersion -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.util.* -import com.sukisu.ultra.ui.util.module.LatestVersionInfo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -class HomeViewModel : ViewModel() { - - // 系统状态 - data class SystemStatus( - val isManager: Boolean = false, - val ksuVersion: Int? = null, - val ksuFullVersion : String? = null, - val lkmMode: Boolean? = null, - val kernelVersion: KernelVersion = getKernelVersion(), - val isRootAvailable: Boolean = false, - val isKpmConfigured: Boolean = false, - val requireNewKernel: Boolean = false - ) - - // 系统信息 - data class SystemInfo( - val kernelRelease: String = "", - val androidVersion: String = "", - val deviceModel: String = "", - val managerVersion: Pair = Pair("", 0L), - val seLinuxStatus: String = "", - val kpmVersion: String = "", - val suSFSStatus: String = "", - val suSFSVersion: String = "", - val suSFSVariant: String = "", - val suSFSFeatures: String = "", - val superuserCount: Int = 0, - val moduleCount: Int = 0, - val kpmModuleCount: Int = 0, - val managersList: Natives.ManagersList? = null, - val isDynamicSignEnabled: Boolean = false, - val zygiskImplement: String = "" - ) - - // 状态变量 - var systemStatus by mutableStateOf(SystemStatus()) - private set - - var systemInfo by mutableStateOf(SystemInfo()) - private set - - var latestVersionInfo by mutableStateOf(LatestVersionInfo()) - private set - - var isSimpleMode by mutableStateOf(false) - private set - var isKernelSimpleMode by mutableStateOf(false) - private set - var isHideVersion by mutableStateOf(false) - private set - var isHideOtherInfo by mutableStateOf(false) - private set - var isHideSusfsStatus by mutableStateOf(false) - private set - var isHideZygiskImplement by mutableStateOf(false) - private set - var isHideLinkCard by mutableStateOf(false) - private set - var showKpmInfo by mutableStateOf(false) - private set - - var isCoreDataLoaded by mutableStateOf(false) - private set - var isExtendedDataLoaded by mutableStateOf(false) - private set - var isRefreshing by mutableStateOf(false) - private set - - // 数据刷新状态流,用于监听变化 - private val _dataRefreshTrigger = MutableStateFlow(0L) - val dataRefreshTrigger: StateFlow = _dataRefreshTrigger - - private var loadingJobs = mutableListOf() - private var lastRefreshTime = 0L - private val refreshCooldown = 2000L - - fun loadUserSettings(context: Context) { - viewModelScope.launch(Dispatchers.IO) { - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false) - isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false) - isHideVersion = settingsPrefs.getBoolean("is_hide_version", false) - isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false) - isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false) - isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false) - isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false) - showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false) - } - } - - fun loadCoreData() { - if (isCoreDataLoaded) return - - val job = viewModelScope.launch(Dispatchers.IO) { - try { - val kernelVersion = getKernelVersion() - val isManager = try { - Natives.isManager - } catch (_: Exception) { - false - } - - val ksuVersion = if (isManager) Natives.version else null - - val fullVersion = try { - Natives.getFullVersion() - } catch (_: Exception) { - "Unknown" - } - - val ksuFullVersion = if (isKernelSimpleMode) { - try { - val startIndex = fullVersion.indexOf('v') - if (startIndex >= 0) { - val endIndex = fullVersion.indexOf('-', startIndex) - val versionStr = if (endIndex > startIndex) { - fullVersion.substring(startIndex, endIndex) - } else { - fullVersion.substring(startIndex) - } - val numericVersion = "v" + (Regex("""\d+(\.\d+)*""").find(versionStr)?.value ?: versionStr) - numericVersion - } else { - fullVersion - } - } catch (_: Exception) { - fullVersion - } - } else { - fullVersion - } - - val lkmMode = ksuVersion?.let { - if (kernelVersion.isGKI()) Natives.isLkmMode else null - } - - val isRootAvailable = try { - rootAvailable() - } catch (_: Exception) { - false - } - - val isKpmConfigured = try { - Natives.isKPMEnabled() - } catch (_: Exception) { - false - } - - val requireNewKernel = try { - isManager && Natives.requireNewKernel() - } catch (_: Exception) { - false - } - - systemStatus = SystemStatus( - isManager = isManager, - ksuVersion = ksuVersion, - ksuFullVersion = ksuFullVersion, - lkmMode = lkmMode, - kernelVersion = kernelVersion, - isRootAvailable = isRootAvailable, - isKpmConfigured = isKpmConfigured, - requireNewKernel = requireNewKernel - ) - - isCoreDataLoaded = true - } catch (_: Exception) { - } - } - loadingJobs.add(job) - } - - fun loadExtendedData(context: Context) { - if (isExtendedDataLoaded) return - - val job = viewModelScope.launch(Dispatchers.IO) { - try { - // 分批加载 - delay(50) - - val basicInfo = loadBasicSystemInfo(context) - systemInfo = systemInfo.copy( - kernelRelease = basicInfo.first, - androidVersion = basicInfo.second, - deviceModel = basicInfo.third, - managerVersion = basicInfo.fourth, - seLinuxStatus = basicInfo.fifth - ) - - delay(100) - - // 加载模块信息 - if (!isSimpleMode) { - val moduleInfo = loadModuleInfo() - systemInfo = systemInfo.copy( - kpmVersion = moduleInfo.first, - superuserCount = moduleInfo.second, - moduleCount = moduleInfo.third, - kpmModuleCount = moduleInfo.fourth, - zygiskImplement = moduleInfo.fifth - ) - } - - delay(100) - - // 加载SuSFS信息 - if (!isHideSusfsStatus) { - val suSFSInfo = loadSuSFSInfo() - systemInfo = systemInfo.copy( - suSFSStatus = suSFSInfo.first, - suSFSVersion = suSFSInfo.second, - suSFSVariant = suSFSInfo.third, - suSFSFeatures = suSFSInfo.fourth, - ) - } - - delay(100) - - // 加载管理器列表 - val managerInfo = loadManagerInfo() - systemInfo = systemInfo.copy( - managersList = managerInfo.first, - isDynamicSignEnabled = managerInfo.second - ) - - isExtendedDataLoaded = true - } catch (_: Exception) { - // 静默处理错误 - } - } - loadingJobs.add(job) - } - - fun refreshData(context: Context, forceRefresh: Boolean = false) { - val currentTime = System.currentTimeMillis() - - // 如果不是强制刷新,检查冷却时间 - if (!forceRefresh && currentTime - lastRefreshTime < refreshCooldown) { - return - } - - lastRefreshTime = currentTime - - viewModelScope.launch { - isRefreshing = true - - try { - // 取消正在进行的加载任务 - loadingJobs.forEach { it.cancel() } - loadingJobs.clear() - - // 重置状态 - isCoreDataLoaded = false - isExtendedDataLoaded = false - - // 触发数据刷新状态流 - _dataRefreshTrigger.value = currentTime - - // 重新加载用户设置 - loadUserSettings(context) - - // 重新加载核心数据 - loadCoreData() - delay(100) - - // 重新加载扩展数据 - loadExtendedData(context) - - // 检查更新 - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val checkUpdate = settingsPrefs.getBoolean("check_update", true) - if (checkUpdate) { - try { - val newVersionInfo = withContext(Dispatchers.IO) { - checkNewVersion() - } - latestVersionInfo = newVersionInfo - } catch (_: Exception) { - } - } - } catch (_: Exception) { - // 静默处理错误 - } finally { - isRefreshing = false - } - } - } - - // 手动触发刷新(下拉刷新使用) - fun onPullRefresh(context: Context) { - refreshData(context, forceRefresh = true) - } - - // 自动刷新数据(当检测到变化时) - fun autoRefreshIfNeeded(context: Context) { - viewModelScope.launch { - // 检查是否需要刷新数据 - val needsRefresh = checkIfDataNeedsRefresh() - if (needsRefresh) { - refreshData(context) - } - } - } - - private suspend fun checkIfDataNeedsRefresh(): Boolean { - return withContext(Dispatchers.IO) { - try { - // 检查KSU状态是否发生变化 - val currentKsuVersion = try { - if (Natives.isManager) { - Natives.version - } else null - } catch (_: Exception) { - null - } - - // 如果KSU版本发生变化,需要刷新 - if (currentKsuVersion != systemStatus.ksuVersion) { - return@withContext true - } - - // 检查模块数量是否发生变化 - val currentModuleCount = try { - getModuleCount() - } catch (_: Exception) { - systemInfo.moduleCount - } - - if (currentModuleCount != systemInfo.moduleCount) { - return@withContext true - } - - false - } catch (_: Exception) { - false - } - } - } - - private suspend fun loadBasicSystemInfo(context: Context): Tuple5, String> { - return withContext(Dispatchers.IO) { - val uname = try { - Os.uname() - } catch (_: Exception) { - null - } - - val deviceModel = try { - getDeviceModel() - } catch (_: Exception) { - "Unknown" - } - - val managerVersion = try { - getManagerVersion(context) - } catch (_: Exception) { - Pair("Unknown", 0L) - } - - val seLinuxStatus = try { - getSELinuxStatus(ksuApp.applicationContext) - } catch (_: Exception) { - "Unknown" - } - - Tuple5( - uname?.release ?: "Unknown", - Build.VERSION.RELEASE ?: "Unknown", - deviceModel, - managerVersion, - seLinuxStatus - ) - } - } - - private suspend fun loadModuleInfo(): Tuple5 { - return withContext(Dispatchers.IO) { - val kpmVersion = try { - getKpmVersion() - } catch (_: Exception) { - "Unknown" - } - - val superuserCount = try { - getSuperuserCount() - } catch (_: Exception) { - 0 - } - - val moduleCount = try { - getModuleCount() - } catch (_: Exception) { - 0 - } - - val kpmModuleCount = try { - getKpmModuleCount() - } catch (_: Exception) { - 0 - } - - val zygiskImplement = try { - getZygiskImplement() - } catch (_: Exception) { - "None" - } - - Tuple5(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement) - } - } - - private suspend fun loadSuSFSInfo(): Tuple4 { - return withContext(Dispatchers.IO) { - val suSFS = try { - val rawFeature = getSuSFSFeatures() - if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) { - "Supported" - } else { - rawFeature - } - } catch (_: Exception) { - "Unknown" - } - - if (suSFS != "Supported") { - return@withContext Tuple4(suSFS, "", "", "") - } - - val suSFSVersion = try { - getSuSFSVersion() - } catch (_: Exception) { - "" - } - - if (suSFSVersion.isEmpty()) { - return@withContext Tuple4(suSFS, "", "", "") - } - - val suSFSVariant = try { - getSuSFSVariant() - } catch (_: Exception) { - "" - } - - val suSFSFeatures = try { - getSuSFSFeatures() - } catch (_: Exception) { - "" - } - - Tuple4(suSFS, suSFSVersion, suSFSVariant, suSFSFeatures) - } - } - - private suspend fun loadManagerInfo(): Pair { - return withContext(Dispatchers.IO) { - val dynamicSignConfig = try { - Natives.getDynamicManager() - } catch (_: Exception) { - null - } - - val isDynamicSignEnabled = try { - dynamicSignConfig?.isValid() == true - } catch (_: Exception) { - false - } - - val managersList = if (isDynamicSignEnabled) { - try { - Natives.getManagersList() - } catch (_: Exception) { - null - } - } else { - null - } - - Pair(managersList, isDynamicSignEnabled) - } - } - - @SuppressLint("PrivateApi") - private fun getDeviceModel(): String { - return try { - val systemProperties = Class.forName("android.os.SystemProperties") - val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java) - val marketNameKeys = listOf( - "ro.product.marketname", - "ro.vendor.oplus.market.name", - "ro.vivo.market.name", - "ro.config.marketing_name" - ) - var result = getDeviceInfo() - for (key in marketNameKeys) { - try { - val marketName = getMethod.invoke(null, key, "") as String - if (marketName.isNotEmpty()) { - result = marketName - break - } - } catch (_: Exception) { - } - } - result - } catch ( - - _: Exception) { - getDeviceInfo() - } - } - - private fun getDeviceInfo(): String { - return try { - var manufacturer = Build.MANUFACTURER ?: "Unknown" - manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1) - - val brand = Build.BRAND ?: "" - if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) { - manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1) - } - - val model = Build.MODEL ?: "" - if (model.isNotEmpty()) { - manufacturer += " $model " - } - - manufacturer - } catch (_: Exception) { - "Unknown Device" - } - } - - private fun getManagerVersion(context: Context): Pair { - return try { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo) - val versionName = packageInfo.versionName ?: "Unknown" - Pair(versionName, versionCode) - } catch (_: Exception) { - Pair("Unknown", 0L) - } - } - - data class Tuple5( - val first: T1, - val second: T2, - val third: T3, - val fourth: T4, - val fifth: T5 - ) - - data class Tuple4( - val first: T1, - val second: T2, - val third: T3, - val fourth: T4 - ) - - override fun onCleared() { - super.onCleared() - loadingJobs.forEach { it.cancel() } - loadingJobs.clear() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt deleted file mode 100644 index 36c5b437..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.sukisu.ultra.ui.viewmodel - -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * @author ShirkNeko - * @date 2025/5/31. - */ -class KpmViewModel : ViewModel() { - var moduleList by mutableStateOf(emptyList()) - private set - - var search by mutableStateOf("") - internal set - - var isRefreshing by mutableStateOf(false) - private set - - var currentModuleDetail by mutableStateOf("") - private set - - fun fetchModuleList() { - viewModelScope.launch { - isRefreshing = true - try { - val moduleCount = getKpmModuleCount() - Log.d("KsuCli", "Module count: $moduleCount") - - moduleList = getAllKpmModuleInfo() - - // 获取 KPM 版本信息 - val kpmVersion = getKpmVersion() - Log.d("KsuCli", "KPM Version: $kpmVersion") - } catch (e: Exception) { - Log.e("KsuCli", "获取模块列表失败", e) - } finally { - isRefreshing = false - } - } - } - - private fun getAllKpmModuleInfo(): List { - val result = mutableListOf() - try { - val str = listKpmModules() - val moduleNames = str - .split("\n") - .filter { it.isNotBlank() } - - for (name in moduleNames) { - try { - val moduleInfo = parseModuleInfo(name) - moduleInfo?.let { result.add(it) } - } catch (e: Exception) { - Log.e("KsuCli", "Error processing module $name", e) - } - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module list", e) - } - return result - } - - private fun parseModuleInfo(name: String): ModuleInfo? { - val info = getKpmModuleInfo(name) - if (info.isBlank()) return null - - val properties = info.lineSequence() - .filter { line -> - val trimmed = line.trim() - trimmed.isNotEmpty() && !trimmed.startsWith("#") - } - .mapNotNull { line -> - line.split("=", limit = 2).let { parts -> - when (parts.size) { - 2 -> parts[0].trim() to parts[1].trim() - 1 -> parts[0].trim() to "" - else -> null - } - } - } - .toMap() - - return ModuleInfo( - id = name, - name = properties["name"] ?: name, - version = properties["version"] ?: "", - author = properties["author"] ?: "", - description = properties["description"] ?: "", - args = properties["args"] ?: "", - enabled = true, - hasAction = true - ) - } - - fun loadModuleDetail(moduleId: String) { - viewModelScope.launch { - try { - currentModuleDetail = withContext(Dispatchers.IO) { - getKpmModuleInfo(moduleId) - } - Log.d("KsuCli", "Module detail loaded: $currentModuleDetail") - } catch (e: Exception) { - Log.e("KsuCli", "Failed to load module detail", e) - currentModuleDetail = "Error: ${e.message}" - } - } - } - - var showInputDialog by mutableStateOf(false) - private set - - var selectedModuleId by mutableStateOf(null) - private set - - var inputArgs by mutableStateOf("") - private set - - fun showInputDialog(moduleId: String) { - selectedModuleId = moduleId - showInputDialog = true - } - - fun hideInputDialog() { - showInputDialog = false - selectedModuleId = null - inputArgs = "" - } - - fun updateInputArgs(args: String) { - inputArgs = args - } - - fun executeControl(): Int { - val moduleId = selectedModuleId ?: return -1 - val result = controlKpmModule(moduleId, inputArgs) - hideInputDialog() - return result - } - - data class ModuleInfo( - val id: String, - val name: String, - val version: String, - val author: String, - val description: String, - val args: String, - val enabled: Boolean, - val hasAction: Boolean - ) -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt index f1cb02ed..9b48f859 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt @@ -1,77 +1,43 @@ package com.sukisu.ultra.ui.viewmodel -import android.content.Context import android.os.SystemClock import android.util.Log +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.dergoogler.mmrl.platform.model.ModuleConfig -import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import com.sukisu.ultra.ksuApp +import com.sukisu.ultra.ui.component.SearchStatus import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.listModules -import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.module.ModuleVerificationManager -import kotlinx.coroutines.withContext +import com.sukisu.ultra.ui.util.overlayFsAvailable import org.json.JSONArray import org.json.JSONObject import java.text.Collator -import java.text.DecimalFormat import java.util.Locale -import java.util.concurrent.TimeUnit -import kotlin.math.log10 -import kotlin.math.pow -import androidx.core.content.edit -/** - * @author ShirkNeko - * @date 2025/5/31. - */ class ModuleViewModel : ViewModel() { companion object { private const val TAG = "ModuleViewModel" private var modules by mutableStateOf>(emptyList()) - private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0" - } - - // 模块大小缓存管理器 - private lateinit var moduleSizeCache: ModuleSizeCache - - fun initializeCache(context: Context) { - if (!::moduleSizeCache.isInitialized) { - moduleSizeCache = ModuleSizeCache(context) - } - } - - fun getModuleSize(dirId: String): String { - if (!::moduleSizeCache.isInitialized) { - return "0 KB" - } - val size = moduleSizeCache.getModuleSize(dirId) - return formatFileSize(size) - } - - /** - * 刷新所有模块的大小缓存 - * 只在安装、卸载、更新模块后调用 - */ - fun refreshModuleSizeCache() { - if (!::moduleSizeCache.isInitialized) return - - viewModelScope.launch(Dispatchers.IO) { - Log.d(TAG, "开始刷新模块大小缓存") - val currentModules = modules.map { it.dirId } - moduleSizeCache.refreshCache(currentModules) - Log.d(TAG, "模块大小缓存刷新完成") - } } + @Immutable class ModuleInfo( val id: String, val name: String, @@ -85,27 +51,63 @@ class ModuleViewModel : ViewModel() { val updateJson: String, val hasWebUi: Boolean, val hasActionScript: Boolean, - val dirId: String, // real module id (dir name) - var config: ModuleConfig? = null, - var isVerified: Boolean = false, // 添加验证状态字段 - var verificationTimestamp: Long = 0L, // 添加验证时间戳 + val metamodule: Boolean, + ) + + @Immutable + data class ModuleUpdateInfo( + val downloadUrl: String, + val version: String, + val changelog: String + ) { + companion object { + val Empty = ModuleUpdateInfo("", "", "") + } + } + + private data class ModuleUpdateSignature( + val updateJson: String, + val versionCode: Int, + val enabled: Boolean, + val update: Boolean, + val remove: Boolean + ) + + private data class ModuleUpdateCache( + val signature: ModuleUpdateSignature, + val info: ModuleUpdateInfo ) var isRefreshing by mutableStateOf(false) private set - var search by mutableStateOf("") + + var isOverlayAvailable by mutableStateOf(false) + private set var sortEnabledFirst by mutableStateOf(false) var sortActionFirst by mutableStateOf(false) + var checkModuleUpdate by mutableStateOf(true) + + private val updateInfoMutex = Mutex() + private var updateInfoCache: MutableMap = mutableMapOf() + private val updateInfoInFlight = mutableSetOf() + private val _updateInfo = mutableStateMapOf() + val updateInfo: SnapshotStateMap = _updateInfo + + private val _searchStatus = mutableStateOf(SearchStatus("")) + val searchStatus: State = _searchStatus + + private val _searchResults = mutableStateOf>(emptyList()) + val searchResults: State> = _searchResults + val moduleList by derivedStateOf { - val comparator = - compareBy( - { if (sortEnabledFirst) !it.enabled else 0 }, - { if (sortActionFirst) !it.hasWebUi && !it.hasActionScript else 0 }, - ).thenBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) + val comparator = moduleComparator() modules.filter { - it.id.contains(search, true) || it.name.contains(search, true) || HanziToPinyin.getInstance() - .toPinyinString(it.name)?.contains(search, true) == true + it.id.contains(searchStatus.value.searchText, true) || it.name.contains( + searchStatus.value.searchText, + true + ) || HanziToPinyin.getInstance() + .toPinyinString(it.name).contains(searchStatus.value.searchText, true) }.sortedWith(comparator).also { isRefreshing = false } @@ -116,113 +118,115 @@ class ModuleViewModel : ViewModel() { fun markNeedRefresh() { isNeedRefresh = true - // 标记需要刷新时,同时刷新大小缓存 - refreshModuleSizeCache() + } + + suspend fun updateSearchText(text: String) { + _searchStatus.value.searchText = text + + if (text.isEmpty()) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.DEFAULT + _searchResults.value = emptyList() + return + } + + val result = withContext(Dispatchers.IO) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.LOAD + modules.filter { + it.id.contains(text, true) || it.name.contains(text, true) || + it.description.contains(text, true) || it.author.contains(text, true) || + HanziToPinyin.getInstance().toPinyinString(it.name).contains(text, true) + }.let { filteredModules -> + val comparator = moduleComparator() + filteredModules.sortedWith(comparator) + } + } + + _searchResults.value = result + _searchStatus.value.resultStatus = if (result.isEmpty()) { + SearchStatus.ResultStatus.EMPTY + } else { + SearchStatus.ResultStatus.SHOW + } + } + + private fun moduleComparator(): Comparator { + return compareBy( + { + val executable = it.hasWebUi || it.hasActionScript + when { + it.metamodule && it.enabled -> 0 + sortEnabledFirst && sortActionFirst -> when { + it.enabled && executable -> 1 + it.enabled -> 2 + executable -> 3 + else -> 4 + } + sortEnabledFirst && !sortActionFirst -> if (it.enabled) 1 else 2 + !sortEnabledFirst && sortActionFirst -> if (executable) 1 else 2 + else -> 1 + } + }, + { if (sortEnabledFirst) !it.enabled else 0 }, + { if (sortActionFirst) !(it.hasWebUi || it.hasActionScript) else 0 }, + ).thenBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) } fun fetchModuleList() { - viewModelScope.launch(Dispatchers.IO) { - isRefreshing = true + viewModelScope.launch { + withContext(Dispatchers.Main) { isRefreshing = true } val oldModuleList = modules - val start = SystemClock.elapsedRealtime() - kotlin.runCatching { - val result = listModules() - - Log.i(TAG, "result: $result") - - val array = JSONArray(result) - val moduleInfos = (0 until array.length()) - .asSequence() - .map { array.getJSONObject(it) } - .map { obj -> - ModuleInfo( - obj.getString("id"), - obj.optString("name"), - obj.optString("author", "Unknown"), - obj.optString("version", "Unknown"), - obj.optInt("versionCode", 0), - obj.optString("description"), - obj.getBoolean("enabled"), - obj.getBoolean("update"), - obj.getBoolean("remove"), - obj.optString("updateJson"), - obj.optBoolean("web"), - obj.optBoolean("action"), - obj.getString("dir_id") - ) - }.toList() - - // 批量检查所有模块的验证状态 - val moduleIds = moduleInfos.map { it.dirId } - val verificationStatus = ModuleVerificationManager.batchCheckVerificationStatus(moduleIds) - - // 更新模块验证状态 - modules = moduleInfos.map { moduleInfo -> - val isVerified = verificationStatus[moduleInfo.dirId] ?: false - val verificationTimestamp = if (isVerified) { - ModuleVerificationManager.getVerificationTimestamp(moduleInfo.dirId) - } else { - 0L - } - - moduleInfo.copy( - isVerified = isVerified, - verificationTimestamp = verificationTimestamp - ) - } - - launch { - modules.forEach { module -> - withContext(Dispatchers.IO) { - try { - runCatching { - module.config = module.id.asModuleConfig - }.onFailure { e -> - Log.e(TAG, "Failed to load config from id for module ${module.id}", e) - } - if (module.config == null) { - runCatching { - module.config = module.name.asModuleConfig - }.onFailure { e -> - Log.e(TAG, "Failed to load config from name for module ${module.id}", e) - } - } - if (module.config == null) { - runCatching { - module.config = module.description.asModuleConfig - }.onFailure { e -> - Log.e(TAG, "Failed to load config from description for module ${module.id}", e) - } - } - if (module.config == null) { - module.config = ModuleConfig() - } - } catch (e: Exception) { - Log.e(TAG, "Failed to load any config for module ${module.id}", e) - module.config = ModuleConfig() - } - } - } - } - - // 首次加载模块列表时,初始化缓存 - if (::moduleSizeCache.isInitialized) { - val currentModules = modules.map { it.dirId } - moduleSizeCache.initializeCacheIfNeeded(currentModules) - } - - isNeedRefresh = false - }.onFailure { e -> - Log.e(TAG, "fetchModuleList: ", e) - isRefreshing = false + val overlayAvailable = withContext(Dispatchers.IO) { + kotlin.runCatching { overlayFsAvailable() }.getOrDefault(false) } - // when both old and new is kotlin.collections.EmptyList - // moduleList update will don't trigger - if (oldModuleList === modules) { + val parsedModules = withContext(Dispatchers.IO) { + kotlin.runCatching { + val result = listModules() + Log.i(TAG, "result: $result") + val array = JSONArray(result) + (0 until array.length()) + .asSequence() + .map { array.getJSONObject(it) } + .map { obj -> + ModuleInfo( + obj.getString("id"), + obj.optString("name"), + obj.optString("author", "Unknown"), + obj.optString("version", "Unknown"), + obj.optInt("versionCode", 0), + obj.optString("description"), + obj.getBoolean("enabled"), + obj.optBoolean("update"), + obj.getBoolean("remove"), + obj.optString("updateJson"), + obj.optBoolean("web"), + obj.optBoolean("action"), + (obj.optInt("metamodule") != 0) or obj.optBoolean("metamodule") + ) + }.toList() + }.getOrElse { + Log.e(TAG, "fetchModuleList: ", it) + emptyList() + } + } + + withContext(Dispatchers.Main) { + isOverlayAvailable = overlayAvailable + modules = parsedModules + isNeedRefresh = false + if (oldModuleList === modules) { + isRefreshing = false + } + } + + if (parsedModules.isNotEmpty()) { + syncModuleUpdateInfo(parsedModules) + } + + withContext(Dispatchers.Main) { isRefreshing = false } @@ -234,50 +238,98 @@ class ModuleViewModel : ViewModel() { return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_") } - fun checkUpdate(m: ModuleInfo): Triple { - val empty = Triple("", "", "") + private fun ModuleInfo.toSignature(): ModuleUpdateSignature { + return ModuleUpdateSignature( + updateJson = updateJson, + versionCode = versionCode, + enabled = enabled, + update = update, + remove = remove + ) + } + + suspend fun syncModuleUpdateInfo(modules: List) { + if (!checkModuleUpdate) return + + val modulesToFetch = mutableListOf>() + val removedIds = mutableSetOf() + + updateInfoMutex.withLock { + val ids = modules.map { it.id }.toSet() + updateInfoCache.keys.filter { it !in ids }.forEach { removedId -> + removedIds += removedId + updateInfoCache.remove(removedId) + updateInfoInFlight.remove(removedId) + } + + modules.forEach { module -> + val signature = module.toSignature() + val cached = updateInfoCache[module.id] + if ((cached == null || cached.signature != signature) && updateInfoInFlight.add(module.id)) { + modulesToFetch += Triple(module.id, module, signature) + } + } + } + + val fetchedEntries = coroutineScope { + modulesToFetch.map { (id, module, signature) -> + async(Dispatchers.IO) { + id to ModuleUpdateCache(signature, checkUpdate(module)) + } + }.awaitAll() + } + + val changedEntries = mutableListOf>() + updateInfoMutex.withLock { + fetchedEntries.forEach { (id, entry) -> + val existing = updateInfoCache[id] + if (existing == null || existing.signature != entry.signature || existing.info != entry.info) { + updateInfoCache[id] = entry + changedEntries += id to entry.info + } + updateInfoInFlight.remove(id) + } + } + + if (removedIds.isEmpty() && changedEntries.isEmpty()) { + return + } + + withContext(Dispatchers.Main) { + removedIds.forEach { _updateInfo.remove(it) } + changedEntries.forEach { (id, info) -> + _updateInfo[id] = info + } + } + } + + fun checkUpdate(m: ModuleInfo): ModuleUpdateInfo { if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) { - return empty + return ModuleUpdateInfo.Empty } // download updateJson val result = kotlin.runCatching { val url = m.updateJson Log.i(TAG, "checkUpdate url: $url") - - val client = okhttp3.OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .build() - - val request = okhttp3.Request.Builder() - .url(url) - .header("User-Agent", CUSTOM_USER_AGENT) - .build() - - val response = client.newCall(request).execute() - + val response = ksuApp.okhttpClient.newCall( + okhttp3.Request.Builder().url(url).build() + ).execute() Log.d(TAG, "checkUpdate code: ${response.code}") if (response.isSuccessful) { response.body?.string() ?: "" } else { - Log.d(TAG, "checkUpdate failed: ${response.message}") "" } - }.getOrElse { e -> - Log.e(TAG, "checkUpdate exception", e) - "" - } - + }.getOrDefault("") Log.i(TAG, "checkUpdate result: $result") if (result.isEmpty()) { - return empty + return ModuleUpdateInfo.Empty } val updateJson = kotlin.runCatching { JSONObject(result) - }.getOrNull() ?: return empty + }.getOrNull() ?: return ModuleUpdateInfo.Empty var version = updateJson.optString("version", "") version = sanitizeVersionString(version) @@ -285,200 +337,9 @@ class ModuleViewModel : ViewModel() { val zipUrl = updateJson.optString("zipUrl", "") val changelog = updateJson.optString("changelog", "") if (versionCode <= m.versionCode || zipUrl.isEmpty()) { - return empty + return ModuleUpdateInfo.Empty } - return Triple(zipUrl, version, changelog) + return ModuleUpdateInfo(zipUrl, version, changelog) } } - -fun ModuleViewModel.ModuleInfo.copy( - id: String = this.id, - name: String = this.name, - author: String = this.author, - version: String = this.version, - versionCode: Int = this.versionCode, - description: String = this.description, - enabled: Boolean = this.enabled, - update: Boolean = this.update, - remove: Boolean = this.remove, - updateJson: String = this.updateJson, - hasWebUi: Boolean = this.hasWebUi, - hasActionScript: Boolean = this.hasActionScript, - dirId: String = this.dirId, - config: ModuleConfig? = this.config, - isVerified: Boolean = this.isVerified, - verificationTimestamp: Long = this.verificationTimestamp -): ModuleViewModel.ModuleInfo { - return ModuleViewModel.ModuleInfo( - id, name, author, version, versionCode, description, - enabled, update, remove, updateJson, hasWebUi, hasActionScript, - dirId, config, isVerified, verificationTimestamp - ) -} - -/** - * 模块大小缓存管理器 - */ -class ModuleSizeCache(context: Context) { - companion object { - private const val TAG = "ModuleSizeCache" - private const val CACHE_PREFS_NAME = "module_size_cache" - private const val CACHE_VERSION_KEY = "cache_version" - private const val CACHE_INITIALIZED_KEY = "cache_initialized" - private const val CURRENT_CACHE_VERSION = 1 - } - - private val cachePrefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) - private val sizeCache = mutableMapOf() - - init { - loadCacheFromPrefs() - } - - /** - * 从SharedPreferences加载缓存 - */ - private fun loadCacheFromPrefs() { - try { - val cacheVersion = cachePrefs.getInt(CACHE_VERSION_KEY, 0) - if (cacheVersion != CURRENT_CACHE_VERSION) { - Log.d(TAG, "缓存版本不匹配,清空缓存") - clearCache() - return - } - - val allEntries = cachePrefs.all - for ((key, value) in allEntries) { - if (key != CACHE_VERSION_KEY && key != CACHE_INITIALIZED_KEY && value is Long) { - sizeCache[key] = value - } - } - Log.d(TAG, "从缓存加载了 ${sizeCache.size} 个模块大小数据") - } catch (e: Exception) { - Log.e(TAG, "加载缓存失败", e) - clearCache() - } - } - - /** - * 保存缓存到SharedPreferences - */ - private fun saveCacheToPrefs() { - try { - cachePrefs.edit { - putInt(CACHE_VERSION_KEY, CURRENT_CACHE_VERSION) - putBoolean(CACHE_INITIALIZED_KEY, true) - - for ((dirId, size) in sizeCache) { - putLong(dirId, size) - } - - } - Log.d(TAG, "保存了 ${sizeCache.size} 个模块大小到缓存") - } catch (e: Exception) { - Log.e(TAG, "保存缓存失败", e) - } - } - - /** - * 获取模块大小(从缓存) - */ - fun getModuleSize(dirId: String): Long { - return sizeCache[dirId] ?: 0L - } - - /** - * 检查缓存是否已初始化,如果没有则初始化 - */ - fun initializeCacheIfNeeded(currentModules: List) { - val isInitialized = cachePrefs.getBoolean(CACHE_INITIALIZED_KEY, false) - if (!isInitialized || sizeCache.isEmpty()) { - Log.d(TAG, "首次初始化缓存,计算所有模块大小") - refreshCache(currentModules) - } else { - // 检查是否有新模块需要计算大小 - val newModules = currentModules.filter { !sizeCache.containsKey(it) } - if (newModules.isNotEmpty()) { - Log.d(TAG, "发现 ${newModules.size} 个新模块,计算大小: $newModules") - for (dirId in newModules) { - val size = calculateModuleFolderSize(dirId) - sizeCache[dirId] = size - Log.d(TAG, "新模块 $dirId 大小: ${formatFileSize(size)}") - } - saveCacheToPrefs() - } - } - } - - /** - * 刷新所有模块的大小缓存 - */ - fun refreshCache(currentModules: List) { - try { - // 清理不存在的模块缓存 - val toRemove = sizeCache.keys.filter { it !in currentModules } - toRemove.forEach { sizeCache.remove(it) } - - if (toRemove.isNotEmpty()) { - Log.d(TAG, "清理了 ${toRemove.size} 个不存在的模块缓存: $toRemove") - } - - // 计算所有当前模块的大小 - for (dirId in currentModules) { - val size = calculateModuleFolderSize(dirId) - sizeCache[dirId] = size - Log.d(TAG, "更新模块 $dirId 大小: ${formatFileSize(size)}") - } - - // 保存到持久化存储 - saveCacheToPrefs() - } catch (e: Exception) { - Log.e(TAG, "刷新缓存失败", e) - } - } - - /** - * 清空所有缓存 - */ - private fun clearCache() { - sizeCache.clear() - cachePrefs.edit { clear() } - Log.d(TAG, "清空所有缓存") - } - - /** - * 实际计算模块文件夹大小 - */ - private fun calculateModuleFolderSize(dirId: String): Long { - return try { - val shell = getRootShell() - val command = "du -sb /data/adb/modules/$dirId" - val result = shell.newJob().add(command).to(ArrayList(), null).exec() - - if (result.isSuccess && result.out.isNotEmpty()) { - val sizeStr = result.out.firstOrNull()?.split("\t")?.firstOrNull() - sizeStr?.toLongOrNull() ?: 0L - } else { - 0L - } - } catch (e: Exception) { - Log.e(TAG, "计算模块大小失败 $dirId: ${e.message}") - 0L - } - } -} - -/** - * 格式化文件大小的工具函数 - */ -fun formatFileSize(bytes: Long): String { - if (bytes <= 0) return "0 KB" - - val units = arrayOf("B", "KB", "MB", "GB", "TB") - val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt() - - return DecimalFormat("#,##0.#").format( - bytes / 1024.0.pow(digitGroups.toDouble()) - ) + " " + units[digitGroups] -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 128c238f..1c820df2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -1,401 +1,230 @@ package com.sukisu.ultra.ui.viewmodel -import android.content.* +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.graphics.drawable.Drawable +import android.os.Build +import android.os.DeadObjectException import android.os.IBinder import android.os.Parcelable +import android.os.RemoteException +import android.os.SystemClock import android.util.Log -import androidx.compose.runtime.* -import androidx.core.content.edit +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ipc.RootService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import com.sukisu.zako.IKsuInterface import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.KsuService -import com.sukisu.ultra.ui.util.* -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import com.sukisu.ultra.ui.component.SearchStatus +import com.sukisu.ultra.ui.util.HanziToPinyin +import com.sukisu.ultra.ui.util.KsuCli import java.text.Collator -import java.util.* -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit +import java.util.Locale import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -import com.sukisu.zako.IKsuInterface -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -enum class AppCategory(val displayNameRes: Int, val persistKey: String) { - ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), - ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"), - CUSTOM(com.sukisu.ultra.R.string.category_custom_apps, "CUSTOM"), - DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT"); - - companion object { - fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL - } -} - -enum class SortType(val displayNameRes: Int, val persistKey: String) { - NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"), - NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"), - INSTALL_TIME_NEW(com.sukisu.ultra.R.string.sort_install_time_new, "INSTALL_TIME_NEW"), - INSTALL_TIME_OLD(com.sukisu.ultra.R.string.sort_install_time_old, "INSTALL_TIME_OLD"), - SIZE_DESC(com.sukisu.ultra.R.string.sort_size_desc, "SIZE_DESC"), - SIZE_ASC(com.sukisu.ultra.R.string.sort_size_asc, "SIZE_ASC"), - USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ"); - - companion object { - fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC - } -} class SuperUserViewModel : ViewModel() { + companion object { private const val TAG = "SuperUserViewModel" private val appsLock = Any() var apps by mutableStateOf>(emptyList()) - private val _isAppListLoaded = MutableStateFlow(false) - val isAppListLoaded = _isAppListLoaded.asStateFlow() @JvmStatic fun getAppIconDrawable(context: Context, packageName: String): Drawable? { val appList = synchronized(appsLock) { apps } - return appList.find { it.packageName == packageName } - ?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) + val appDetail = appList.find { it.packageName == packageName } + return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) } - - var appGroups by mutableStateOf>(emptyList()) - - private const val PREFS_NAME = "settings" - private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" - private const val KEY_SELECTED_CATEGORY = "selected_category" - private const val KEY_CURRENT_SORT_TYPE = "current_sort_type" - private const val CORE_POOL_SIZE = 8 - private const val MAX_POOL_SIZE = 16 - private const val KEEP_ALIVE_TIME = 60L - private const val BATCH_SIZE = 20 } - @Immutable + + private var _appList = mutableStateOf>(emptyList()) + val appList: State> = _appList + private val _searchStatus = mutableStateOf(SearchStatus("")) + val searchStatus: State = _searchStatus + @Parcelize data class AppInfo( val label: String, val packageInfo: PackageInfo, val profile: Natives.Profile?, ) : Parcelable { - @IgnoredOnParcel - val packageName: String = packageInfo.packageName - @IgnoredOnParcel - val uid: Int = packageInfo.applicationInfo!!.uid + val packageName: String + get() = packageInfo.packageName + val uid: Int + get() = packageInfo.applicationInfo!!.uid + + val allowSu: Boolean + get() = profile != null && profile.allowSu + val hasCustomProfile: Boolean + get() { + if (profile == null) { + return false + } + + return if (profile.allowSu) { + !profile.rootUseDefault + } else { + !profile.nonRootUseDefault + } + } } - @Immutable - @Parcelize - data class AppGroup( - val uid: Int, - val apps: List, - val profile: Natives.Profile? - ) : Parcelable { - @IgnoredOnParcel - val mainApp: AppInfo = apps.first() - @IgnoredOnParcel - val packageNames: List = apps.map { it.packageName } - @IgnoredOnParcel - val allowSu: Boolean = profile?.allowSu == true - @IgnoredOnParcel - val userName: String? = Natives.getUserName(uid) - @IgnoredOnParcel - val hasCustomProfile : Boolean = profile?.let { if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault } ?: false - } - - private val appProcessingThreadPool = ThreadPoolExecutor( - CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, - LinkedBlockingQueue() - ) { runnable -> - Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply { - isDaemon = true - priority = Thread.NORM_PRIORITY - } - }.asCoroutineDispatcher() - - private val appListMutex = Mutex() - private val configChangeListeners = mutableSetOf<(String) -> Unit>() - private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - - var search by mutableStateOf("") - var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)) - private set - var selectedCategory by mutableStateOf(loadSelectedCategory()) - private set - var currentSortType by mutableStateOf(loadCurrentSortType()) - private set + var showSystemApps by mutableStateOf(false) var isRefreshing by mutableStateOf(false) private set - var showBatchActions by mutableStateOf(false) - internal set - var selectedApps by mutableStateOf>(emptySet()) - internal set - var loadingProgress by mutableFloatStateOf(0f) - private set - private fun loadSelectedCategory(): AppCategory { - val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) - ?: AppCategory.ALL.persistKey - return AppCategory.fromPersistKey(categoryKey) - } + private val _searchResults = mutableStateOf>(emptyList()) + val searchResults: State> = _searchResults - private fun loadCurrentSortType(): SortType { - val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) - ?: SortType.NAME_ASC.persistKey - return SortType.fromPersistKey(sortKey) - } + suspend fun updateSearchText(text: String) { + _searchStatus.value.searchText = text - fun updateShowSystemApps(newValue: Boolean) { - showSystemApps = newValue - prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) } - notifyAppListChanged() - } + if (text.isEmpty()) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.DEFAULT + _searchResults.value = emptyList() + return + } - private fun notifyAppListChanged() { - val currentApps = apps - apps = emptyList() - apps = currentApps - } + val result = withContext(Dispatchers.IO) { + _searchStatus.value.resultStatus = SearchStatus.ResultStatus.LOAD + _appList.value.filter { + it.label.contains(_searchStatus.value.searchText, true) || it.packageName.contains( + _searchStatus.value.searchText, + true + ) || HanziToPinyin.getInstance().toPinyinString(it.label) + .contains(_searchStatus.value.searchText, true) + } + } - fun updateSelectedCategory(newCategory: AppCategory) { - selectedCategory = newCategory - prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) } - } - - fun updateCurrentSortType(newSortType: SortType) { - currentSortType = newSortType - prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) } - } - - fun toggleBatchMode() { - showBatchActions = !showBatchActions - if (!showBatchActions) clearSelection() - } - - fun toggleAppSelection(packageName: String) { - selectedApps = if (selectedApps.contains(packageName)) { - selectedApps - packageName + if (_searchResults.value == result) { + fetchAppList() + updateSearchText(text) } else { - selectedApps + packageName + _searchResults.value = result + } + _searchStatus.value.resultStatus = if (result.isEmpty()) { + SearchStatus.ResultStatus.EMPTY + } else { + SearchStatus.ResultStatus.SHOW + } + } - fun clearSelection() { - selectedApps = emptySet() - } + private suspend inline fun connectKsuService( + crossinline onDisconnect: () -> Unit = {} + ): Pair = suspendCoroutine { + val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnect() + } - suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) { - selectedApps.forEach { packageName -> - apps.find { it.packageName == packageName }?.let { app -> - val profile = Natives.getAppProfile(packageName, app.uid) - val updatedProfile = profile.copy( - allowSu = allowSu, - umountModules = umountModules ?: profile.umountModules, - nonRootUseDefault = false - ) - if (Natives.setAppProfile(updatedProfile)) { - updateAppProfileLocally(packageName, updatedProfile) - notifyConfigChange(packageName) - } + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + it.resume(binder as IBinder to this) } } - clearSelection() - showBatchActions = false - refreshAppConfigurations() + + val intent = Intent(ksuApp, KsuService::class.java) + + val task = RootService.bindOrTask( + intent, + Shell.EXECUTOR, + connection, + ) + val shell = KsuCli.SHELL + task?.let { it1 -> shell.execTask(it1) } } - fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { - appListMutex.tryLock().let { locked -> - if (locked) { - try { - apps = apps.map { app -> - if (app.packageName == packageName) { - app.copy(profile = updatedProfile) - } else app - } - } finally { - appListMutex.unlock() - } - } - } - } - - private fun notifyConfigChange(packageName: String) { - configChangeListeners.forEach { listener -> - try { - listener(packageName) - } catch (e: Exception) { - Log.e(TAG, "Error notifying config change for $packageName", e) - } - } - } - - suspend fun refreshAppConfigurations() { - withContext(appProcessingThreadPool) { - supervisorScope { - val currentApps = apps.toList() - val batches = currentApps.chunked(BATCH_SIZE) - loadingProgress = 0f - - val updatedApps = batches.mapIndexed { batchIndex, batch -> - async { - val batchResult = batch.map { app -> - try { - val updatedProfile = Natives.getAppProfile(app.packageName, app.uid) - app.copy(profile = updatedProfile) - } catch (e: Exception) { - Log.e(TAG, "Error refreshing profile for ${app.packageName}", e) - app - } - } - loadingProgress = (batchIndex + 1).toFloat() / batches.size - batchResult - } - }.awaitAll().flatten() - - appListMutex.withLock { apps = updatedApps } - loadingProgress = 1f - } - } - } - - private var serviceConnection: ServiceConnection? = null - - private suspend fun connectKsuService(onDisconnect: () -> Unit = {}): IBinder? = - suspendCoroutine { continuation -> - val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - onDisconnect() - serviceConnection = null - } - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - continuation.resume(binder) - } - } - serviceConnection = connection - val intent = Intent(ksuApp, KsuService::class.java) - try { - val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( - intent, Shell.EXECUTOR, connection - ) - task?.let { Shell.getShell().execTask(it) } - } catch (e: Exception) { - Log.e(TAG, "Failed to bind KsuService", e) - continuation.resume(null) - } - } - private fun stopKsuService() { - serviceConnection?.let { - try { - val intent = Intent(ksuApp, KsuService::class.java) - com.topjohnwu.superuser.ipc.RootService.stop(intent) - serviceConnection = null - } catch (e: Exception) { - Log.e(TAG, "Failed to stop KsuService", e) - } - } + val intent = Intent(ksuApp, KsuService::class.java) + RootService.stop(intent) } suspend fun fetchAppList() { - isRefreshing = true - loadingProgress = 0f + Mutex().withLock { + withContext(Dispatchers.Main) { isRefreshing = true } - val binder = connectKsuService() ?: run { isRefreshing = false; return } + val result = connectKsuService { + Log.w(TAG, "KsuService disconnected") + } - withContext(Dispatchers.IO) { - val pm = ksuApp.packageManager - val allPackages = IKsuInterface.Stub.asInterface(binder) - val total = allPackages.packageCount - val pageSize = 100 - val result = mutableListOf() + val allPackagesSlice = withContext(Dispatchers.IO) { + val pm = ksuApp.packageManager + val start = SystemClock.elapsedRealtime() - var start = 0 - while (start < total) { - val page = allPackages.getPackages(start, pageSize) - if (page.isEmpty()) break + val binder = result.first + val iface = IKsuInterface.Stub.asInterface(binder) + val slice = try { + iface.getPackages(0) + } catch (_: DeadObjectException) { + val retry = connectKsuService { Log.w(TAG, "KsuService disconnected") } + IKsuInterface.Stub.asInterface(retry.first).getPackages(0) + } catch (_: RemoteException) { + val retry = connectKsuService { Log.w(TAG, "KsuService disconnected") } + IKsuInterface.Stub.asInterface(retry.first).getPackages(0) + } - result += page.mapNotNull { packageInfo -> - packageInfo.applicationInfo?.let { appInfo -> - AppInfo( - label = appInfo.loadLabel(pm).toString(), - packageInfo = packageInfo, - profile = Natives.getAppProfile(packageInfo.packageName, appInfo.uid) - ) + val packages = slice.list + val newApps = packages.map { + val appInfo = it.applicationInfo + val uid = appInfo!!.uid + val profile = Natives.getAppProfile(it.packageName, uid) + AppInfo( + label = appInfo.loadLabel(pm).toString(), + packageInfo = it, + profile = profile, + ) + }.filter { it.packageName != ksuApp.packageName } + .filter { + val ai = it.packageInfo.applicationInfo!! + if (Build.VERSION.SDK_INT >= 29) !ai.isResourceOverlay else true } + + val comparator = compareBy { + when { + it.allowSu -> 0 + it.hasCustomProfile -> 1 + else -> 2 + } + }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) + val sortedFiltered = newApps.sortedWith(comparator).filter { + it.uid == 2000 + || showSystemApps + || it.allowSu + || it.hasCustomProfile + || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } - start += page.size - loadingProgress = start.toFloat() / total + + Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") + + Pair(newApps, sortedFiltered) } - stopKsuService() - - synchronized(appsLock) { - _isAppListLoaded.value = true + withContext(Dispatchers.Main) { + synchronized(appsLock) { + apps = allPackagesSlice.first + } + _appList.value = allPackagesSlice.second + isRefreshing = false + stopKsuService() } - - appListMutex.withLock { - val filteredApps = result.filter { it.packageName != ksuApp.packageName } - apps = filteredApps - appGroups = groupAppsByUid(filteredApps) - } - loadingProgress = 1f - } - isRefreshing = false - } - - val appGroupList by derivedStateOf { - appGroups.filter { group -> - group.apps.any { app -> - app.label.contains(search, true) || - app.packageName.contains(search, true) || - HanziToPinyin.getInstance().toPinyinString(app.label)?.contains(search, true) == true - } - }.filter { group -> - group.uid == 2000 || showSystemApps || - group.apps.any { it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } } } - - private fun groupAppsByUid(appList: List): List { - return appList.groupBy { it.uid } - .map { (uid, apps) -> - val sortedApps = apps.sortedBy { it.label } - val profile = apps.firstOrNull()?.let { Natives.getAppProfile(it.packageName, uid) } - AppGroup(uid = uid, apps = sortedApps, profile = profile) - } - .sortedWith( - compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 - } - }.thenBy(Collator.getInstance(Locale.getDefault())) { - it.userName?.takeIf { name -> name.isNotBlank() } ?: it.uid.toString() - }.thenBy(Collator.getInstance(Locale.getDefault())) { it.mainApp.label } - ) } - override fun onCleared() { - super.onCleared() - try { - stopKsuService() - appProcessingThreadPool.close() - configChangeListeners.clear() - } catch (e: Exception) { - Log.e(TAG, "Error cleaning up resources", e) - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt index 7aa5b945..15ba6795 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt @@ -7,22 +7,21 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import com.sukisu.ultra.Natives +import com.sukisu.ultra.ksuApp import com.sukisu.ultra.profile.Capabilities import com.sukisu.ultra.profile.Groups import com.sukisu.ultra.ui.util.getAppProfileTemplate import com.sukisu.ultra.ui.util.listAppProfileTemplates import com.sukisu.ultra.ui.util.setAppProfileTemplate -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONArray import org.json.JSONObject import java.text.Collator -import java.util.* -import java.util.concurrent.TimeUnit +import java.util.Locale /** @@ -36,7 +35,6 @@ const val TAG = "TemplateViewModel" class TemplateViewModel : ViewModel() { companion object { - private var templates by mutableStateOf>(emptyList()) } @@ -138,13 +136,7 @@ class TemplateViewModel : ViewModel() { private fun fetchRemoteTemplates() { runCatching { - val client: OkHttpClient = OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .build() - - client.newCall( + ksuApp.okhttpClient.newCall( Request.Builder().url(TEMPLATE_INDEX_URL).build() ).execute().use { response -> if (!response.isSuccessful) { @@ -155,7 +147,7 @@ private fun fetchRemoteTemplates() { 0.until(remoteTemplateIds.length()).forEach { i -> val id = remoteTemplateIds.getString(i) Log.i(TAG, "fetch template: $id") - val templateJson = client.newCall( + val templateJson = ksuApp.okhttpClient.newCall( Request.Builder().url(TEMPLATE_URL.format(id)).build() ).runCatching { execute().use { response -> @@ -219,11 +211,11 @@ private fun getLocaleString(json: JSONObject, key: String): String { val localeKey = "${locale.language}_${locale.country}" json.optJSONObject("locales")?.let { // check locale first - it.optJSONObject(localeKey)?.let { json-> + it.optJSONObject(localeKey)?.let { json -> return json.optString(key, fallback) } // fallback to language - it.optJSONObject(locale.language)?.let { json-> + it.optJSONObject(locale.language)?.let { json -> return json.optString(key, fallback) } } @@ -281,8 +273,9 @@ fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject { put("gid", template.gid) if (template.groups.isNotEmpty()) { - put("groups", JSONArray( - Groups.entries.filter { + put( + "groups", JSONArray( + Groups.entries.filter { template.groups.contains(it.gid) }.map { it.name @@ -291,8 +284,9 @@ fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject { } if (template.capabilities.isNotEmpty()) { - put("capabilities", JSONArray( - Capabilities.entries.filter { + put( + "capabilities", JSONArray( + Capabilities.entries.filter { template.capabilities.contains(it.cap) }.map { it.name diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt index 361a976b..b6215237 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt @@ -43,4 +43,4 @@ object AppIconUtil { drawable.draw(canvas) return bmp } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt index aabdbe2a..f4b15fbd 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt @@ -37,4 +37,4 @@ data class Insets( appendLine("\t--f7-safe-area-right: var(--window-inset-right, 0px) !important;") append("}") } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt deleted file mode 100644 index dad41e34..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.sukisu.ultra.ui.webui - -import android.content.ServiceConnection -import android.util.Log -import com.dergoogler.mmrl.platform.Platform -import com.dergoogler.mmrl.platform.model.IProvider -import com.dergoogler.mmrl.platform.model.PlatformIntent -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp -import com.topjohnwu.superuser.ipc.RootService -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext - -class KsuLibSuProvider : IProvider { - override val name = "KsuLibSu" - - override fun isAvailable() = true - - override suspend fun isAuthorized() = Natives.isManager - - private val serviceIntent - get() = PlatformIntent( - ksuApp, - Platform.KsuNext, - SuService::class.java - ) - - override fun bind(connection: ServiceConnection) { - RootService.bind(serviceIntent.intent, connection) - } - - override fun unbind(connection: ServiceConnection) { - RootService.stop(serviceIntent.intent) - } -} - -// webui x -suspend fun initPlatform() = withContext(Dispatchers.IO) { - try { - val active = Platform.init { - this.context = ksuApp - this.platform = Platform.KsuNext - this.provider = from(KsuLibSuProvider()) - } - - while (!active) { - delay(1000) - } - - return@withContext true - } catch (e: Exception) { - Log.e("KsuLibSu", "Failed to initialize platform", e) - return@withContext false - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java new file mode 100644 index 00000000..5a801039 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sukisu.ultra.ui.webui; + +import java.net.URLConnection; + +class MimeUtil { + + public static String getMimeFromFileName(String fileName) { + if (fileName == null) { + return null; + } + + // Copying the logic and mapping that Chromium follows. + // First we check against the OS (this is a limited list by default) + // but app developers can extend this. + // We then check against a list of hardcoded mime types above if the + // OS didn't provide a result. + String mimeType = URLConnection.guessContentTypeFromName(fileName); + + if (mimeType != null) { + return mimeType; + } + + return guessHardcodedMime(fileName); + } + + // We should keep this map in sync with the lists under + // //net/base/mime_util.cc in Chromium. + // A bunch of the mime types don't really apply to Android land + // like word docs so feel free to filter out where necessary. + private static String guessHardcodedMime(String fileName) { + int finalFullStop = fileName.lastIndexOf('.'); + if (finalFullStop == -1) { + return null; + } + + final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); + + return switch (extension) { + case "webm" -> "video/webm"; + case "mpeg", "mpg" -> "video/mpeg"; + case "mp3" -> "audio/mpeg"; + case "wasm" -> "application/wasm"; + case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; + case "flac" -> "audio/flac"; + case "ogg", "oga", "opus" -> "audio/ogg"; + case "wav" -> "audio/wav"; + case "m4a" -> "audio/x-m4a"; + case "gif" -> "image/gif"; + case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; + case "png" -> "image/png"; + case "apng" -> "image/apng"; + case "svg", "svgz" -> "image/svg+xml"; + case "webp" -> "image/webp"; + case "mht", "mhtml" -> "multipart/related"; + case "css" -> "text/css"; + case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; + case "js", "mjs" -> "application/javascript"; + case "xml" -> "text/xml"; + case "mp4", "m4v" -> "video/mp4"; + case "ogv", "ogm" -> "video/ogg"; + case "ico" -> "image/x-icon"; + case "woff" -> "application/font-woff"; + case "gz", "tgz" -> "application/gzip"; + case "json" -> "application/json"; + case "pdf" -> "application/pdf"; + case "zip" -> "application/zip"; + case "bmp" -> "image/bmp"; + case "tiff", "tif" -> "image/tiff"; + default -> null; + }; + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt deleted file mode 100644 index b9adcfa2..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.sukisu.ultra.ui.webui - -import java.net.URLConnection - -internal object MimeUtil { - fun getMimeFromFileName(fileName: String?): String? { - if (fileName == null) { - return null - } - - val mimeType = URLConnection.guessContentTypeFromName(fileName) - if (mimeType != null) { - return mimeType - } - - return guessHardcodedMime(fileName) - } - - private fun guessHardcodedMime(fileName: String): String? { - val finalFullStop = fileName.lastIndexOf('.') - if (finalFullStop == -1) { - return null - } - - val extension = fileName.substring(finalFullStop + 1).lowercase() - - return when (extension) { - "webm" -> "video/webm" - "mpeg", "mpg" -> "video/mpeg" - "mp3" -> "audio/mpeg" - "wasm" -> "application/wasm" - "xhtml", "xht", "xhtm" -> "application/xhtml+xml" - "flac" -> "audio/flac" - "ogg", "oga", "opus" -> "audio/ogg" - "wav" -> "audio/wav" - "m4a" -> "audio/x-m4a" - "gif" -> "image/gif" - "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg" - "png" -> "image/png" - "apng" -> "image/apng" - "svg", "svgz" -> "image/svg+xml" - "webp" -> "image/webp" - "mht", "mhtml" -> "multipart/related" - "css" -> "text/css" - "html", "htm", "shtml", "shtm", "ehtml" -> "text/html" - "js", "mjs" -> "application/javascript" - "xml" -> "text/xml" - "mp4", "m4v" -> "video/mp4" - "ogv", "ogm" -> "video/ogg" - "ico" -> "image/x-icon" - "woff" -> "application/font-woff" - "gz", "tgz" -> "application/gzip" - "json" -> "application/json" - "pdf" -> "application/pdf" - "zip" -> "application/zip" - "bmp" -> "image/bmp" - "tiff", "tif" -> "image/tiff" - else -> null - } - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java new file mode 100644 index 00000000..1b57e3e0 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java @@ -0,0 +1,210 @@ +package com.sukisu.ultra.ui.webui; + +import android.content.Context; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; +import androidx.webkit.WebViewAssetLoader; + +import com.topjohnwu.superuser.Shell; +import com.topjohnwu.superuser.io.SuFile; +import com.topjohnwu.superuser.io.SuFileInputStream; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; + +/** + * Handler class to open files from file system by root access + * For more information about android storage please refer to + * Android Developers + * Docs: Data and file storage overview. + *

+ * To avoid leaking user or app data to the web, make sure to choose {@code directory} + * carefully, and assume any file under this directory could be accessed by any web page subject + * to same-origin rules. + *

+ * A typical usage would be like: + *

+ * File publicDir = new File(context.getFilesDir(), "public");
+ * // Host "files/public/" in app's data directory under:
+ * // http://appassets.androidplatform.net/public/...
+ * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
+ *          .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
+ *          .build();
+ * 
+ */ +public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler { + private static final String TAG = "SuFilePathHandler"; + + /** + * Default value to be used as MIME type if guessing MIME type failed. + */ + public static final String DEFAULT_MIME_TYPE = "text/plain"; + + /** + * Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this + * handler. They are forbidden as they often contain sensitive information. + *

+ * Note: Any future addition to this list will be considered breaking changes to the API. + */ + private static final String[] FORBIDDEN_DATA_DIRS = + new String[] {"/data/data", "/data/system"}; + + @NonNull + private final File mDirectory; + + private final Shell mShell; + private final InsetsSupplier mInsetsSupplier; + + public interface InsetsSupplier { + @NonNull + Insets get(); + } + + /** + * Creates PathHandler for app's internal storage. + * The directory to be exposed must be inside either the application's internal data + * directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}. + * External storage is not supported for security reasons, as other apps with + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the + * files. + *

+ * Exposing the entire data or cache directory is not permitted, to avoid accidentally + * exposing sensitive application files to the web. Certain existing subdirectories of + * {@link Context#getDataDir} are also not permitted as they are often sensitive. + * These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"}, + * {@code "shared_prefs/"} and {@code "code_cache/"}). + *

+ * The application should typically use a dedicated subdirectory for the files it intends to + * expose and keep them separate from other files. + * + * @param context {@link Context} that is used to access app's internal storage. + * @param directory the absolute path of the exposed app internal storage directory from + * which files can be loaded. + * @param rootShell {@link Shell} instance with root access to read files. + * @param insetsSupplier {@link InsetsSupplier} to provide window insets for styling web content. + * @throws IllegalArgumentException if the directory is not allowed. + */ + public SuFilePathHandler(@NonNull Context context, @NonNull File directory, Shell rootShell, @NonNull InsetsSupplier insetsSupplier) { + try { + mInsetsSupplier = insetsSupplier; + mDirectory = new File(getCanonicalDirPath(directory)); + if (!isAllowedInternalStorageDir(context)) { + throw new IllegalArgumentException("The given directory \"" + directory + + "\" doesn't exist under an allowed app internal storage directory"); + } + mShell = rootShell; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to resolve the canonical path for the given directory: " + + directory.getPath(), e); + } + } + + private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException { + String dir = getCanonicalDirPath(mDirectory); + + for (String forbiddenPath : FORBIDDEN_DATA_DIRS) { + if (dir.startsWith(forbiddenPath)) { + return false; + } + } + return true; + } + + /** + * Opens the requested file from the exposed data directory. + *

+ * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the + * requested file cannot be found or is outside the mounted directory a + * {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be + * returned instead of {@code null}. This saves the time of falling back to network and + * trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with + * {@code null} {@link InputStream} will be received as an HTTP response with status code + * {@code 404} and no body. + *

+ * The MIME type for the file will be determined from the file's extension using + * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that + * files are named using standard file extensions. If the file does not have a + * recognised extension, {@code "text/plain"} will be used by default. + * + * @param path the suffix path to be handled. + * @return {@link WebResourceResponse} for the requested file. + */ + @Override + @WorkerThread + @NonNull + public WebResourceResponse handle(@NonNull String path) { + if ("internal/insets.css".equals(path)) { + String css = mInsetsSupplier.get().getCss(); + return new WebResourceResponse( + "text/css", + "utf-8", + new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8)) + ); + } + try { + File file = getCanonicalFileIfChild(mDirectory, path); + if (file != null) { + InputStream is = openFile(file, mShell); + String mimeType = guessMimeType(path); + return new WebResourceResponse(mimeType, null, is); + } else { + Log.e(TAG, String.format( + "The requested file: %s is outside the mounted directory: %s", path, + mDirectory)); + } + } catch (IOException e) { + Log.e(TAG, "Error opening the requested path: " + path, e); + } + return new WebResourceResponse(null, null, null); + } + + public static String getCanonicalDirPath(@NonNull File file) throws IOException { + String canonicalPath = file.getCanonicalPath(); + if (!canonicalPath.endsWith("/")) canonicalPath += "/"; + return canonicalPath; + } + + public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child) + throws IOException { + String parentCanonicalPath = getCanonicalDirPath(parent); + String childCanonicalPath = new File(parent, child).getCanonicalPath(); + if (childCanonicalPath.startsWith(parentCanonicalPath)) { + return new File(childCanonicalPath); + } + return null; + } + + @NonNull + private static InputStream handleSvgzStream(@NonNull String path, + @NonNull InputStream stream) throws IOException { + return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream; + } + + public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException { + SuFile suFile = new SuFile(file.getAbsolutePath()); + suFile.setShell(shell); + InputStream fis = SuFileInputStream.open(suFile); + return handleSvgzStream(file.getPath(), fis); + } + + /** + * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the + * {@link #DEFAULT_MIME_TYPE} if it can't guess. + * + * @param filePath path of the file to guess its MIME type. + * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}. + */ + @NonNull + public static String guessMimeType(@NonNull String filePath) { + String mimeType = MimeUtil.getMimeFromFileName(filePath); + return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt deleted file mode 100644 index c0f79308..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.sukisu.ultra.ui.webui - -import android.content.Context -import android.util.Log -import android.webkit.WebResourceResponse -import androidx.annotation.WorkerThread -import androidx.webkit.WebViewAssetLoader -import com.topjohnwu.superuser.Shell -import com.topjohnwu.superuser.io.SuFile -import com.topjohnwu.superuser.io.SuFileInputStream -import java.io.ByteArrayInputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.nio.charset.StandardCharsets -import java.util.zip.GZIPInputStream - -/** - * Handler class to open files from file system by root access - * For more information about android storage please refer to - * [Android Developers Docs: Data and file storage overview](https://developer.android.com/guide/topics/data/data-storage). - * - * To avoid leaking user or app data to the web, make sure to choose [directory] - * carefully, and assume any file under this directory could be accessed by any web page subject - * to same-origin rules. - * - * A typical usage would be like: - * ``` - * val publicDir = File(context.filesDir, "public") - * // Host "files/public/" in app's data directory under: - * // http://appassets.androidplatform.net/public/... - * val assetLoader = WebViewAssetLoader.Builder() - * .addPathHandler("/public/", SuFilePathHandler(context, publicDir, shell, insetsSupplier)) - * .build() - * ``` - */ -class SuFilePathHandler( - directory: File, - private val shell: Shell, - private val insetsSupplier: InsetsSupplier -) : WebViewAssetLoader.PathHandler { - - private val directory: File - - init { - try { - this.directory = File(getCanonicalDirPath(directory)) - if (!isAllowedInternalStorageDir()) { - throw IllegalArgumentException( - "The given directory \"$directory\" doesn't exist under an allowed app internal storage directory" - ) - } - } catch (e: IOException) { - throw IllegalArgumentException( - "Failed to resolve the canonical path for the given directory: ${directory.path}", - e - ) - } - } - - fun interface InsetsSupplier { - fun get(): Insets - } - - private fun isAllowedInternalStorageDir(): Boolean { - return try { - val dir = getCanonicalDirPath(directory) - FORBIDDEN_DATA_DIRS.none { dir.startsWith(it) } - } catch (_: IOException) { - false - } - } - - /** - * Opens the requested file from the exposed data directory. - * - * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the - * requested file cannot be found or is outside the mounted directory a - * [WebResourceResponse] object with a `null` [InputStream] will be - * returned instead of `null`. This saves the time of falling back to network and - * trying to resolve a path that doesn't exist. A [WebResourceResponse] with - * `null` [InputStream] will be received as an HTTP response with status code - * `404` and no body. - * - * The MIME type for the file will be determined from the file's extension using - * [java.net.URLConnection.guessContentTypeFromName]. Developers should ensure that - * files are named using standard file extensions. If the file does not have a - * recognised extension, `"text/plain"` will be used by default. - * - * @param path the suffix path to be handled. - * @return [WebResourceResponse] for the requested file. - */ - @WorkerThread - override fun handle(path: String): WebResourceResponse { - if (path == "internal/insets.css") { - val css = insetsSupplier.get().css - return WebResourceResponse( - "text/css", - "utf-8", - ByteArrayInputStream(css.toByteArray(StandardCharsets.UTF_8)) - ) - } - - try { - val file = getCanonicalFileIfChild(directory, path) - if (file != null) { - val inputStream = openFile(file, shell) - val mimeType = guessMimeType(path) - return WebResourceResponse(mimeType, null, inputStream) - } else { - Log.e( - TAG, - "The requested file: $path is outside the mounted directory: $directory" - ) - } - } catch (e: IOException) { - Log.e(TAG, "Error opening the requested path: $path", e) - } - - return WebResourceResponse(null, null, null) - } - - companion object { - private const val TAG = "SuFilePathHandler" - - /** - * Default value to be used as MIME type if guessing MIME type failed. - */ - const val DEFAULT_MIME_TYPE = "text/plain" - - /** - * Forbidden subdirectories of [Context.getDataDir] that cannot be exposed by this - * handler. They are forbidden as they often contain sensitive information. - * - * Note: Any future addition to this list will be considered breaking changes to the API. - */ - private val FORBIDDEN_DATA_DIRS = arrayOf("/data/data", "/data/system") - - @JvmStatic - @Throws(IOException::class) - fun getCanonicalDirPath(file: File): String { - var canonicalPath = file.canonicalPath - if (!canonicalPath.endsWith("/")) { - canonicalPath += "/" - } - return canonicalPath - } - - @JvmStatic - @Throws(IOException::class) - fun getCanonicalFileIfChild(parent: File, child: String): File? { - val parentCanonicalPath = getCanonicalDirPath(parent) - val childCanonicalPath = File(parent, child).canonicalPath - return if (childCanonicalPath.startsWith(parentCanonicalPath)) { - File(childCanonicalPath) - } else { - null - } - } - - @Throws(IOException::class) - private fun handleSvgzStream(path: String, stream: InputStream): InputStream { - return if (path.endsWith(".svgz")) { - GZIPInputStream(stream) - } else { - stream - } - } - - @JvmStatic - @Throws(IOException::class) - fun openFile(file: File, shell: Shell): InputStream { - val suFile = SuFile(file.absolutePath).apply { - setShell(shell) - } - val fis = SuFileInputStream.open(suFile) - return handleSvgzStream(file.path, fis) - } - - /** - * Use [MimeUtil.getMimeFromFileName] to guess MIME type or return the - * [DEFAULT_MIME_TYPE] if it can't guess. - * - * @param filePath path of the file to guess its MIME type. - * @return MIME type guessed from file extension or [DEFAULT_MIME_TYPE]. - */ - @JvmStatic - fun guessMimeType(filePath: String): String { - return MimeUtil.getMimeFromFileName(filePath) ?: DEFAULT_MIME_TYPE - } - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuService.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuService.kt deleted file mode 100644 index 5a421f24..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sukisu.ultra.ui.webui - -import android.content.Intent -import android.os.IBinder -import com.dergoogler.mmrl.platform.model.PlatformIntent.Companion.getPlatform -import com.dergoogler.mmrl.platform.service.ServiceManager -import com.topjohnwu.superuser.ipc.RootService - -class SuService : RootService() { - override fun onBind(intent: Intent): IBinder { - val mode = intent.getPlatform() - return ServiceManager(mode) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt index 91ecd6c2..02c7712c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt @@ -14,27 +14,26 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.webkit.WebViewAssetLoader -import com.dergoogler.mmrl.platform.model.ModId -import com.dergoogler.mmrl.webui.interfaces.WXOptions +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.launch import com.sukisu.ultra.ui.util.createRootShell import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import java.io.File @SuppressLint("SetJavaScriptEnabled") class WebUIActivity : ComponentActivity() { - private val rootShell by lazy { createRootShell(true) } + private lateinit var webviewInterface: WebViewInterface + private var rootShell: Shell? = null private lateinit var insets: Insets - private var webView = null as WebView? override fun onCreate(savedInstanceState: Bundle?) { @@ -51,24 +50,26 @@ class WebUIActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + InfiniteProgressIndicator() } } + val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java] + lifecycleScope.launch { - SuperUserViewModel.isAppListLoaded.first { it } + superUserViewModel.fetchAppList() setupWebView() } } + private fun setupWebView() { - val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return } - val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return } + val moduleId = intent.getStringExtra("id")!! + val name = intent.getStringExtra("name")!! if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { @Suppress("DEPRECATION") - setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name")) + setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) } else { - val taskDescription = - ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build() + val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() setTaskDescription(taskDescription) } @@ -77,12 +78,14 @@ class WebUIActivity : ComponentActivity() { val moduleDir = "/data/adb/modules/${moduleId}" val webRoot = File("${moduleDir}/webroot") + val rootShell = createRootShell(true).also { this.rootShell = it } insets = Insets(0, 0, 0, 0) + val webViewAssetLoader = WebViewAssetLoader.Builder() .setDomain("mui.kernelsu.org") .addPathHandler( "/", - SuFilePathHandler(webRoot, rootShell) { insets } + SuFilePathHandler(this, webRoot, rootShell) { insets } ) .build() @@ -92,6 +95,7 @@ class WebUIActivity : ComponentActivity() { request: WebResourceRequest ): WebResourceResponse? { val url = request.url + // Handle ksu://icon/[packageName] to serve app icon via WebView if (url.scheme.equals("ksu", ignoreCase = true) && url.host.equals("icon", ignoreCase = true)) { val packageName = url.path?.substring(1) @@ -105,13 +109,12 @@ class WebUIActivity : ComponentActivity() { } } } + return webViewAssetLoader.shouldInterceptRequest(url) } } val webView = WebView(this).apply { - webView = this - setBackgroundColor(Color.TRANSPARENT) val density = resources.displayMetrics.density @@ -128,7 +131,8 @@ class WebUIActivity : ComponentActivity() { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.allowFileAccess = false - addJavascriptInterface(WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId))), "ksu") + webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir) + addJavascriptInterface(webviewInterface, "ksu") setWebViewClient(webViewClient) loadUrl("https://mui.kernelsu.org/index.html") } @@ -137,13 +141,7 @@ class WebUIActivity : ComponentActivity() { } override fun onDestroy() { - rootShell.runCatching { close() } - webView?.apply { - stopLoading() - removeAllViews() - destroy() - webView = null - } super.onDestroy() + runCatching { rootShell?.close() } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt deleted file mode 100644 index 0761274b..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.sukisu.ultra.ui.webui - -import android.app.ActivityManager -import android.os.Build -import android.os.Bundle -import android.webkit.WebView -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.* -import androidx.lifecycle.lifecycleScope -import com.dergoogler.mmrl.platform.Platform -import com.dergoogler.mmrl.platform.model.ModId -import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.webui.model.WebUIConfig -import com.dergoogler.mmrl.webui.screen.WebUIScreen -import com.dergoogler.mmrl.webui.util.rememberWebUIOptions -import com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.ui.theme.KernelSUTheme -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -class WebUIXActivity : ComponentActivity() { - private lateinit var webView: WebView - - private val userAgent - get(): String { - val ksuVersion = BuildConfig.VERSION_CODE - - val platform = Platform.get("Unknown") { - platform.name - } - - val platformVersion = Platform.get(-1) { - moduleManager.versionCode - } - - val osVersion = Build.VERSION.RELEASE - val deviceModel = Build.MODEL - - return "SukiSU-Ultra /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - - webView = WebView(this) - - lifecycleScope.launch { - initPlatform() - } - - val moduleId = intent.getStringExtra("id")!! - val name = intent.getStringExtra("name")!! - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - @Suppress("DEPRECATION") - setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name")) - } else { - val taskDescription = - ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build() - setTaskDescription(taskDescription) - } - - val prefs = getSharedPreferences("settings", MODE_PRIVATE) - - setContent { - KernelSUTheme { - var isLoading by remember { mutableStateOf(true) } - - LaunchedEffect(Platform.isAlive) { - while (!Platform.isAlive) { - delay(1000) - } - - isLoading = false - } - - if (isLoading) { - Loading() - return@KernelSUTheme - } - - val webDebugging = prefs.getBoolean("enable_web_debugging", false) - val erudaInject = prefs.getBoolean("use_webuix_eruda", false) - val dark = isSystemInDarkTheme() - - val options = rememberWebUIOptions( - modId = ModId(moduleId), - debug = webDebugging, - appVersionCode = BuildConfig.VERSION_CODE, - isDarkMode = dark, - enableEruda = erudaInject, - cls = WebUIXActivity::class.java, - userAgentString = userAgent - ) - - // idk why webuix not allow root impl change webuiConfig - // so we use magic to force exitConfirm shutdown - val field = WebUIConfig::class.java.getDeclaredField("exitConfirm") - field.isAccessible = true - field.set(options.config, false) - field.isAccessible = false - - WebUIScreen( - webView = webView, - options = options, - interfaces = listOf( - WebViewInterface.factory() - ) - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt index 1e271046..3824efbe 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt @@ -1,40 +1,35 @@ package com.sukisu.ultra.ui.webui import android.app.Activity +import android.content.Context import android.content.pm.ApplicationInfo import android.os.Handler import android.os.Looper import android.text.TextUtils import android.view.Window import android.webkit.JavascriptInterface +import android.webkit.WebView import android.widget.Toast import androidx.core.content.pm.PackageInfoCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import com.dergoogler.mmrl.webui.interfaces.WXInterface -import com.dergoogler.mmrl.webui.interfaces.WXOptions -import com.dergoogler.mmrl.webui.model.JavaScriptInterface -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.internal.UiThreadHandler +import com.sukisu.ultra.ui.util.createRootShell +import com.sukisu.ultra.ui.util.listModules +import com.sukisu.ultra.ui.util.withNewRootShell +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import org.json.JSONArray import org.json.JSONObject import java.io.File import java.util.concurrent.CompletableFuture -@Suppress("unused") class WebViewInterface( - wxOptions: WXOptions, -) : WXInterface(wxOptions) { - override var name: String = "ksu" - - companion object { - fun factory() = JavaScriptInterface(WebViewInterface::class.java) - } - - private val modDir get() = "/data/adb/modules/${modId.id}" + val context: Context, + private val webView: WebView, + private val modDir: String +) { @JavascriptInterface fun exec(cmd: String): String { @@ -69,56 +64,56 @@ class WebViewInterface( options: String?, callbackFunc: String ) { - val finalCommand = buildString { - processOptions(this, options) - append(cmd) - } + val finalCommand = StringBuilder() + processOptions(finalCommand, options) + finalCommand.append(cmd) val result = withNewRootShell(true) { - newJob().add(finalCommand).to(ArrayList(), ArrayList()).exec() + newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec() } val stdout = result.out.joinToString(separator = "\n") val stderr = result.err.joinToString(separator = "\n") val jsCode = - "(function() { try { ${callbackFunc}(${result.code}, ${ + "javascript: (function() { try { ${callbackFunc}(${result.code}, ${ JSONObject.quote( stdout ) }, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();" webView.post { - webView.evaluateJavascript(jsCode, null) + webView.loadUrl(jsCode) } } @JavascriptInterface fun spawn(command: String, args: String, options: String?, callbackFunc: String) { - val finalCommand = buildString { - processOptions(this, options) + val finalCommand = StringBuilder() - if (!TextUtils.isEmpty(args)) { - append(command).append(" ") - JSONArray(args).let { argsArray -> - for (i in 0 until argsArray.length()) { - append("${argsArray.getString(i)} ") - } + processOptions(finalCommand, options) + + if (!TextUtils.isEmpty(args)) { + finalCommand.append(command).append(" ") + JSONArray(args).let { argsArray -> + for (i in 0 until argsArray.length()) { + finalCommand.append(argsArray.getString(i)) + finalCommand.append(" ") } - } else { - append(command) } + } else { + finalCommand.append(command) } val shell = createRootShell(true) val emitData = fun(name: String, data: String) { val jsCode = - "(function() { try { ${callbackFunc}.${name}.emit('data', ${ + "javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${ JSONObject.quote( data ) }); } catch(e) { console.error('emitData', e); } })();" webView.post { - webView.evaluateJavascript(jsCode, null) + webView.loadUrl(jsCode) } } @@ -134,21 +129,21 @@ class WebViewInterface( } } - val future = shell.newJob().add(finalCommand).to(stdout, stderr).enqueue() + val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue() val completableFuture = CompletableFuture.supplyAsync { future.get() } completableFuture.thenAccept { result -> val emitExitCode = - $$"(function() { try { $${callbackFunc}.emit('exit', $${result.code}); } catch(e) { console.error(`emitExit error: ${e}`); } })();" + "javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();" webView.post { - webView.evaluateJavascript(emitExitCode, null) + webView.loadUrl(emitExitCode) } if (result.code != 0) { val emitErrCode = - "(function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${ + "javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${ JSONObject.quote( result.err.joinToString( "\n" @@ -156,7 +151,7 @@ class WebViewInterface( ) };${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();" webView.post { - webView.evaluateJavascript(emitErrCode, null) + webView.loadUrl(emitErrCode) } } }.whenComplete { _, _ -> @@ -176,9 +171,9 @@ class WebViewInterface( if (context is Activity) { Handler(Looper.getMainLooper()).post { if (enable) { - hideSystemUI(activity.window) + hideSystemUI(context.window) } else { - showSystemUI(activity.window) + showSystemUI(context.window) } } } @@ -187,7 +182,7 @@ class WebViewInterface( @JavascriptInterface fun moduleInfo(): String { val moduleInfos = JSONArray(listModules()) - val currentModuleInfo = JSONObject() + var currentModuleInfo = JSONObject() currentModuleInfo.put("moduleDir", modDir) val moduleId = File(modDir).getName() for (i in 0 until moduleInfos.length()) { @@ -197,7 +192,7 @@ class WebViewInterface( continue } - val keys = currentInfo.keys() + var keys = currentInfo.keys() for (key in keys) { currentModuleInfo.put(key, currentInfo.get(key)) } @@ -255,18 +250,6 @@ class WebViewInterface( } return jsonArray.toString() } - - // =================== KPM支持 ============================= - - @JavascriptInterface - fun listAllKpm(): String { - return listKpmModules() - } - - @JavascriptInterface - fun controlKpm(name: String, args: String): Int { - return controlKpmModule(name, args) - } } fun hideSystemUI(window: Window) = @@ -276,4 +259,4 @@ fun hideSystemUI(window: Window) = } fun showSystemUI(window: Window) = - WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) \ No newline at end of file + WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) diff --git a/manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt deleted file mode 100644 index 91ad7c79..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.sukisu.ultra.utils - -import android.content.Context -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - -object AssetsUtil { - @Throws(IOException::class) - fun exportFiles(context: Context, src: String, out: String) { - val fileNames = context.assets.list(src) - if (fileNames?.isNotEmpty() == true) { - val file = File(out) - file.mkdirs() - fileNames.forEach { fileName -> - exportFiles(context, "$src/$fileName", "$out/$fileName") - } - } else { - context.assets.open(src).use { inputStream -> - FileOutputStream(File(out)).use { outputStream -> - inputStream.copyTo(outputStream) - } - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt deleted file mode 100644 index a87558f9..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt +++ /dev/null @@ -1,475 +0,0 @@ -package zako.zako.zako.zakoui.screen.kernelFlash - -import android.content.Context -import android.net.Uri -import android.os.Environment -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.KeyEventBlocker -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.reboot -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import zako.zako.zako.zakoui.screen.kernelFlash.state.FlashState -import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelState -import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelWorker -import java.io.File -import java.text.SimpleDateFormat -import java.util.* - -/** - * @author ShirkNeko - * @date 2025/5/31. - */ -private object KernelFlashStateHolder { - var currentState: HorizonKernelState? = null - var currentUri: Uri? = null - var currentSlot: String? = null - var currentKpmPatchEnabled: Boolean = false - var currentKpmUndoPatch: Boolean = false - var isFlashing = false -} - -/** - * Kernel刷写界面 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun KernelFlashScreen( - navigator: DestinationsNavigator, - kernelUri: Uri, - selectedSlot: String? = null, - kpmPatchEnabled: Boolean = false, - kpmUndoPatch: Boolean = false -) { - val context = LocalContext.current - - val shouldAutoExit = remember { - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.getBoolean("auto_exit_after_flash", false) - } - - val scrollState = rememberScrollState() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current - val scope = rememberCoroutineScope() - var logText by rememberSaveable { mutableStateOf("") } - var showFloatAction by rememberSaveable { mutableStateOf(false) } - val logContent = rememberSaveable { StringBuilder() } - val horizonKernelState = remember { - if (KernelFlashStateHolder.currentState != null && - KernelFlashStateHolder.currentUri == kernelUri && - KernelFlashStateHolder.currentSlot == selectedSlot && - KernelFlashStateHolder.currentKpmPatchEnabled == kpmPatchEnabled && - KernelFlashStateHolder.currentKpmUndoPatch == kpmUndoPatch) { - KernelFlashStateHolder.currentState!! - } else { - HorizonKernelState().also { - KernelFlashStateHolder.currentState = it - KernelFlashStateHolder.currentUri = kernelUri - KernelFlashStateHolder.currentSlot = selectedSlot - KernelFlashStateHolder.currentKpmPatchEnabled = kpmPatchEnabled - KernelFlashStateHolder.currentKpmUndoPatch = kpmUndoPatch - KernelFlashStateHolder.isFlashing = false - } - } - } - - val flashState by horizonKernelState.state.collectAsState() - val logSavedString = stringResource(R.string.log_saved) - - val onFlashComplete = { - showFloatAction = true - KernelFlashStateHolder.isFlashing = false - - // 如果需要自动退出,延迟1.5秒后退出 - if (shouldAutoExit) { - scope.launch { - delay(1500) - val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) - sharedPref.edit { remove("auto_exit_after_flash") } - (context as? ComponentActivity)?.finish() - } - } - } - - // 开始刷写 - LaunchedEffect(Unit) { - if (!KernelFlashStateHolder.isFlashing && !flashState.isCompleted && flashState.error.isEmpty()) { - withContext(Dispatchers.IO) { - KernelFlashStateHolder.isFlashing = true - val worker = HorizonKernelWorker( - context = context, - state = horizonKernelState, - slot = selectedSlot, - kpmPatchEnabled = kpmPatchEnabled, - kpmUndoPatch = kpmUndoPatch - ) - worker.uri = kernelUri - worker.setOnFlashCompleteListener(onFlashComplete) - worker.start() - - // 监听日志更新 - while (flashState.error.isEmpty()) { - if (flashState.logs.isNotEmpty()) { - logText = flashState.logs.joinToString("\n") - logContent.clear() - logContent.append(logText) - } - delay(100) - } - - if (flashState.error.isNotEmpty()) { - logText += "\n${flashState.error}\n" - logContent.append("\n${flashState.error}\n") - KernelFlashStateHolder.isFlashing = false - } - } - } else { - logText = flashState.logs.joinToString("\n") - if (flashState.error.isNotEmpty()) { - logText += "\n${flashState.error}\n" - } else if (flashState.isCompleted) { - logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n" - showFloatAction = true - } - } - } - - val onBack: () -> Unit = { - if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) { - // 清理全局状态 - if (flashState.isCompleted || flashState.error.isNotEmpty()) { - KernelFlashStateHolder.currentState = null - KernelFlashStateHolder.currentUri = null - KernelFlashStateHolder.currentSlot = null - KernelFlashStateHolder.currentKpmPatchEnabled = false - KernelFlashStateHolder.currentKpmUndoPatch = false - KernelFlashStateHolder.isFlashing = false - } - navigator.popBackStack() - } - } - - DisposableEffect(shouldAutoExit) { - onDispose { - if (shouldAutoExit) { - KernelFlashStateHolder.currentState = null - KernelFlashStateHolder.currentUri = null - KernelFlashStateHolder.currentSlot = null - KernelFlashStateHolder.currentKpmPatchEnabled = false - KernelFlashStateHolder.currentKpmUndoPatch = false - KernelFlashStateHolder.isFlashing = false - } - } - } - - BackHandler(enabled = true) { - onBack() - } - - Scaffold( - topBar = { - TopBar( - flashState = flashState, - onBack = onBack, - onSave = { - scope.launch { - val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) - val date = format.format(Date()) - val file = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "KernelSU_kernel_flash_log_${date}.log" - ) - file.writeText(logContent.toString()) - snackBarHost.showSnackbar(logSavedString.format(file.absolutePath)) - } - }, - scrollBehavior = scrollBehavior - ) - }, - floatingActionButton = { - if (showFloatAction) { - ExtendedFloatingActionButton( - onClick = { - scope.launch { - withContext(Dispatchers.IO) { - reboot() - } - } - }, - icon = { - Icon( - Icons.Filled.Refresh, - contentDescription = stringResource(id = R.string.reboot) - ) - }, - text = { - Text(text = stringResource(id = R.string.reboot)) - }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - expanded = true - ) - } - }, - snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - containerColor = MaterialTheme.colorScheme.background - ) { innerPadding -> - KeyEventBlocker { - it.key == Key.VolumeDown || it.key == Key.VolumeUp - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .nestedScroll(scrollBehavior.nestedScrollConnection), - ) { - FlashProgressIndicator(flashState, kpmPatchEnabled, kpmUndoPatch) - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(scrollState) - ) { - LaunchedEffect(logText) { - scrollState.animateScrollTo(scrollState.maxValue) - } - Text( - modifier = Modifier.padding(16.dp), - text = logText, - style = MaterialTheme.typography.bodyMedium, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } -} - -@Composable -private fun FlashProgressIndicator( - flashState: FlashState, - kpmPatchEnabled: Boolean = false, - kpmUndoPatch: Boolean = false -) { - val progressColor = when { - flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error - flashState.isCompleted -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.primary - } - - val progress = animateFloatAsState( - targetValue = flashState.progress, - label = "FlashProgress" - ) - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = when { - flashState.error.isNotEmpty() -> stringResource(R.string.flash_failed) - flashState.isCompleted -> stringResource(R.string.flash_success) - else -> stringResource(R.string.flashing) - }, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = progressColor - ) - - when { - flashState.error.isNotEmpty() -> { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } - flashState.isCompleted -> { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.tertiary - ) - } - } - } - - // KPM状态显示 - if (kpmPatchEnabled || kpmUndoPatch) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = if (kpmUndoPatch) stringResource(R.string.kpm_undo_patch_mode) - else stringResource(R.string.kpm_patch_mode), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.tertiary - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - if (flashState.currentStep.isNotEmpty()) { - Text( - text = flashState.currentStep, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(8.dp)) - } - - LinearProgressIndicator( - progress = { progress.value }, - modifier = Modifier - .fillMaxWidth() - .height(8.dp), - color = progressColor, - trackColor = MaterialTheme.colorScheme.surfaceVariant - ) - - if (flashState.error.isNotEmpty()) { - Spacer(modifier = Modifier.height(8.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(16.dp) - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = flashState.error, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), - shape = MaterialTheme.shapes.small - ) - .padding(8.dp) - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - flashState: FlashState, - onBack: () -> Unit, - onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val statusColor = when { - flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error - flashState.isCompleted -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.primary - } - - val colorScheme = MaterialTheme.colorScheme - val cardColor = if (CardConfig.isCustomBackgroundEnabled) { - colorScheme.surfaceContainerLow - } else { - colorScheme.background - } - val cardAlpha = CardConfig.cardAlpha - - TopAppBar( - title = { - Text( - text = stringResource( - when { - flashState.error.isNotEmpty() -> R.string.flash_failed - flashState.isCompleted -> R.string.flash_success - else -> R.string.kernel_flashing - } - ), - style = MaterialTheme.typography.titleLarge, - color = statusColor - ) - }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - actions = { - IconButton(onClick = onSave) { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = stringResource(id = R.string.save_log), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt deleted file mode 100644 index 26da72c5..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt +++ /dev/null @@ -1,258 +0,0 @@ -package zako.zako.zako.zakoui.screen.kernelFlash.component - -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.material.icons.Icons -import androidx.compose.material.icons.filled.SdStorage -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.R - -/** - * 槽位选择对话框组件 - * 用于Kernel刷写时选择目标槽位 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SlotSelectionDialog( - show: Boolean, - onDismiss: () -> Unit, - onSlotSelected: (String) -> Unit -) { - var currentSlot by remember { mutableStateOf(null) } - var errorMessage by remember { mutableStateOf(null) } - var selectedSlot by remember { mutableStateOf(null) } - - LaunchedEffect(Unit) { - try { - currentSlot = getCurrentSlot() - // 设置默认选择为当前槽位 - selectedSlot = when (currentSlot) { - "a" -> "a" - "b" -> "b" - else -> null - } - errorMessage = null - } catch (e: Exception) { - errorMessage = e.message - currentSlot = null - } - } - - if (show) { - val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(id = R.string.select_slot_title), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - if (errorMessage != null) { - Text( - text = "Error: $errorMessage", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) - } else { - Text( - text = stringResource( - id = R.string.current_slot, - currentSlot ?: "Unknown" - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = stringResource(id = R.string.select_slot_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(24.dp)) - - // Horizontal arrangement for slot options with highlighted current slot - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val slotOptions = listOf( - ListOption( - titleText = stringResource(id = R.string.slot_a), - subtitleText = null, - icon = Icons.Filled.SdStorage - ), - ListOption( - titleText = stringResource(id = R.string.slot_b), - subtitleText = 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 (selectedSlot == when(index) { - 0 -> "a" - else -> "b" - }) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) - } else { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } - ) - .clickable { - selectedSlot = when(index) { - 0 -> "a" - else -> "b" - } - } - .padding(vertical = 12.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = option.icon, - contentDescription = null, - tint = if (selectedSlot == when(index) { - 0 -> "a" - else -> "b" - }) { - 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 (selectedSlot == when(index) { - 0 -> "a" - else -> "b" - }) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.primary - } - ) - option.subtitleText?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = if (selectedSlot == when(index) { - 0 -> "a" - else -> "b" - }) { - MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } - } - } - } - } - } - } - }, - confirmButton = { - TextButton( - onClick = { - selectedSlot?.let { onSlotSelected(it) } - onDismiss() - }, - enabled = selectedSlot != null - ) { - 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(): String? { - return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let { - if (it.startsWith("_")) it.substring(1) else it - } -} - -private fun runCommandGetOutput(su: Boolean, cmd: String): String? { - return try { - val process = ProcessBuilder(if (su) "su" else "sh").start() - process.outputStream.bufferedWriter().use { writer -> - writer.write("$cmd\n") - writer.write("exit\n") - writer.flush() - } - process.inputStream.bufferedReader().use { reader -> - reader.readText().trim() - } - } catch (_: Exception) { - null - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt deleted file mode 100644 index 8cfa76ba..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt +++ /dev/null @@ -1,524 +0,0 @@ -package zako.zako.zako.zakoui.screen.kernelFlash.state - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import com.sukisu.ultra.R -import com.sukisu.ultra.network.RemoteToolsDownloader -import com.sukisu.ultra.ui.util.install -import com.sukisu.ultra.ui.util.rootAvailable -import com.sukisu.ultra.utils.AssetsUtil -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - - -/** - * @author ShirkNeko - * @date 2025/5/31. - */ -data class FlashState( - val isFlashing: Boolean = false, - val isCompleted: Boolean = false, - val progress: Float = 0f, - val currentStep: String = "", - val logs: List = emptyList(), - val error: String = "" -) - -class HorizonKernelState { - private val _state = MutableStateFlow(FlashState()) - val state: StateFlow = _state.asStateFlow() - - fun updateProgress(progress: Float) { - _state.update { it.copy(progress = progress) } - } - - fun updateStep(step: String) { - _state.update { it.copy(currentStep = step) } - } - - fun addLog(log: String) { - _state.update { - it.copy(logs = it.logs + log) - } - } - - fun setError(error: String) { - _state.update { it.copy(error = error) } - } - - fun startFlashing() { - _state.update { - it.copy( - isFlashing = true, - isCompleted = false, - progress = 0f, - currentStep = "under preparation...", - logs = emptyList(), - error = "" - ) - } - } - - fun completeFlashing() { - _state.update { it.copy(isCompleted = true, progress = 1f) } - } - - fun reset() { - _state.value = FlashState() - } -} - -class HorizonKernelWorker( - private val context: Context, - private val state: HorizonKernelState, - private val slot: String? = null, - private val kpmPatchEnabled: Boolean = false, - private val kpmUndoPatch: Boolean = false -) : Thread() { - var uri: Uri? = null - private lateinit var filePath: String - private lateinit var binaryPath: String - private lateinit var workDir: String - - private var onFlashComplete: (() -> Unit)? = null - private var originalSlot: String? = null - private var downloaderJob: Job? = null - - fun setOnFlashCompleteListener(listener: () -> Unit) { - onFlashComplete = listener - } - - override fun run() { - state.startFlashing() - state.updateStep(context.getString(R.string.horizon_preparing)) - - filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}" - binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary" - workDir = "${context.filesDir.absolutePath}/work" - - try { - state.updateStep(context.getString(R.string.horizon_cleaning_files)) - state.updateProgress(0.1f) - cleanup() - - if (!rootAvailable()) { - state.setError(context.getString(R.string.root_required)) - return - } - - state.updateStep(context.getString(R.string.horizon_copying_files)) - state.updateProgress(0.2f) - copy() - - if (!File(filePath).exists()) { - state.setError(context.getString(R.string.horizon_copy_failed)) - return - } - - state.updateStep(context.getString(R.string.horizon_extracting_tool)) - state.updateProgress(0.4f) - getBinary() - - // KPM修补 - if (kpmPatchEnabled || kpmUndoPatch) { - state.updateStep(context.getString(R.string.kpm_preparing_tools)) - state.updateProgress(0.5f) - prepareKpmToolsWithDownload() - - state.updateStep( - if (kpmUndoPatch) context.getString(R.string.kpm_undoing_patch) - else context.getString(R.string.kpm_applying_patch) - ) - state.updateProgress(0.55f) - performKpmPatch() - } - - state.updateStep(context.getString(R.string.horizon_patching_script)) - state.updateProgress(0.6f) - patch() - - state.updateStep(context.getString(R.string.horizon_flashing)) - state.updateProgress(0.7f) - - val isAbDevice = isAbDevice() - - if (isAbDevice && slot != null) { - state.updateStep(context.getString(R.string.horizon_getting_original_slot)) - state.updateProgress(0.72f) - originalSlot = runCommandGetOutput("getprop ro.boot.slot_suffix") - - state.updateStep(context.getString(R.string.horizon_setting_target_slot)) - state.updateProgress(0.74f) - runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot") - } - - flash() - - if (isAbDevice && !originalSlot.isNullOrEmpty()) { - state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) - state.updateProgress(0.8f) - runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") - } - - try { - install() - } catch (e: Exception) { - state.updateStep("ksud update skipped: ${e.message}") - } - - state.updateStep(context.getString(R.string.horizon_flash_complete_status)) - state.completeFlashing() - - (context as? Activity)?.runOnUiThread { - onFlashComplete?.invoke() - } - } catch (e: Exception) { - state.setError(e.message ?: context.getString(R.string.horizon_unknown_error)) - - if (isAbDevice() && !originalSlot.isNullOrEmpty()) { - state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) - state.updateProgress(0.8f) - runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") - } - } finally { - // 取消下载任务并清理 - downloaderJob?.cancel() - cleanupDownloader() - } - } - - private fun prepareKpmToolsWithDownload() { - try { - File(workDir).mkdirs() - val downloader = RemoteToolsDownloader(context, workDir) - - val progressListener = object : RemoteToolsDownloader.DownloadProgressListener { - override fun onProgress(fileName: String, progress: Int, total: Int) { - val percentage = if (total > 0) (progress * 100) / total else 0 - state.addLog("Downloading $fileName: $percentage% ($progress/$total bytes)") - } - - override fun onLog(message: String) { - state.addLog(message) - } - - override fun onError(fileName: String, error: String) { - state.addLog("Warning: $fileName - $error") - } - - override fun onSuccess(fileName: String, isRemote: Boolean) { - val source = if (isRemote) "remote" else "local" - state.addLog("✓ $fileName $source version prepared successfully") - } - } - - val downloadJob = CoroutineScope(Dispatchers.IO).launch { - downloader.downloadToolsAsync(progressListener) - } - - downloaderJob = downloadJob - - runBlocking { - downloadJob.join() - } - - val kptoolsPath = "$workDir/kptools" - val kpimgPath = "$workDir/kpimg" - - if (!File(kptoolsPath).exists()) { - throw IOException("kptools file preparation failed") - } - - if (!File(kpimgPath).exists()) { - throw IOException("kpimg file preparation failed") - } - - runCommand(true, "chmod a+rx $kptoolsPath") - state.addLog("KPM tools preparation completed, starting patch operation") - - } catch (_: CancellationException) { - state.addLog("KPM tools download cancelled") - throw IOException("Tool preparation process interrupted") - } catch (e: Exception) { - state.addLog("KPM tools preparation failed: ${e.message}") - - state.addLog("Attempting to use legacy local file extraction...") - try { - prepareKpmToolsLegacy() - state.addLog("Successfully used local backup files") - } catch (legacyException: Exception) { - state.addLog("Local file extraction also failed: ${legacyException.message}") - throw IOException("Unable to prepare KPM tool files: ${e.message}") - } - } - } - - private fun prepareKpmToolsLegacy() { - File(workDir).mkdirs() - - val kptoolsPath = "$workDir/kptools" - val kpimgPath = "$workDir/kpimg" - - AssetsUtil.exportFiles(context, "kptools", kptoolsPath) - if (!File(kptoolsPath).exists()) { - throw IOException("Local kptools file extraction failed") - } - - AssetsUtil.exportFiles(context, "kpimg", kpimgPath) - if (!File(kpimgPath).exists()) { - throw IOException("Local kpimg file extraction failed") - } - - runCommand(true, "chmod a+rx $kptoolsPath") - } - - private fun cleanupDownloader() { - try { - val downloader = RemoteToolsDownloader(context, workDir) - downloader.cleanup() - } catch (_: Exception) { - } - } - - /** - * 执行KPM修补操作 - */ - private fun performKpmPatch() { - try { - // 创建临时解压目录 - val extractDir = "$workDir/extracted" - File(extractDir).mkdirs() - - // 解压压缩包到临时目录 - val unzipResult = runCommand(true, "cd $extractDir && unzip -o \"$filePath\"") - if (unzipResult != 0) { - throw IOException(context.getString(R.string.kpm_extract_zip_failed)) - } - - // 查找Image文件 - val findImageResult = runCommandGetOutput("find $extractDir -name '*Image*' -type f") - if (findImageResult.isBlank()) { - throw IOException(context.getString(R.string.kpm_image_file_not_found)) - } - - val imageFile = findImageResult.lines().first().trim() - val imageDir = File(imageFile).parent - val imageName = File(imageFile).name - - state.addLog(context.getString(R.string.kpm_found_image_file, imageFile)) - - // 复制KPM工具到Image文件所在目录 - runCommand(true, "cp $workDir/kptools $imageDir/") - runCommand(true, "cp $workDir/kpimg $imageDir/") - - // 执行KPM修补命令 - val patchCommand = if (kpmUndoPatch) { - "cd $imageDir && chmod a+rx kptools && ./kptools -u -s 123 -i $imageName -k kpimg -o oImage && mv oImage $imageName" - } else { - "cd $imageDir && chmod a+rx kptools && ./kptools -p -s 123 -i $imageName -k kpimg -o oImage && mv oImage $imageName" - } - - val patchResult = runCommand(true, patchCommand) - if (patchResult != 0) { - throw IOException( - if (kpmUndoPatch) context.getString(R.string.kpm_undo_patch_failed) - else context.getString(R.string.kpm_patch_failed) - ) - } - - state.addLog( - if (kpmUndoPatch) context.getString(R.string.kpm_undo_patch_success) - else context.getString(R.string.kpm_patch_success) - ) - - // 清理KPM工具文件 - runCommand(true, "rm -f $imageDir/kptools $imageDir/kpimg $imageDir/oImage") - - // 重新打包ZIP文件 - val originalFileName = File(filePath).name - val patchedFilePath = "$workDir/patched_$originalFileName" - - repackZipFolder(extractDir, patchedFilePath) - - // 替换原始文件 - runCommand(true, "mv \"$patchedFilePath\" \"$filePath\"") - - state.addLog(context.getString(R.string.kpm_file_repacked)) - - } catch (e: Exception) { - state.addLog(context.getString(R.string.kpm_patch_operation_failed, e.message)) - throw e - } finally { - // 清理临时文件 - runCommand(true, "rm -rf $workDir") - } - } - - private fun repackZipFolder(sourceDir: String, zipFilePath: String) { - try { - val buffer = ByteArray(1024) - val sourceFolder = File(sourceDir) - - FileOutputStream(zipFilePath).use { fos -> - ZipOutputStream(fos).use { zos -> - sourceFolder.walkTopDown().forEach { file -> - if (file.isFile) { - val relativePath = file.relativeTo(sourceFolder).path - val zipEntry = ZipEntry(relativePath) - zos.putNextEntry(zipEntry) - - file.inputStream().use { fis -> - var length: Int - while (fis.read(buffer).also { length = it } > 0) { - zos.write(buffer, 0, length) - } - } - - zos.closeEntry() - } - } - } - } - } catch (e: Exception) { - throw IOException("Failed to create zip file: ${e.message}", e) - } - } - - // 检查设备是否为AB分区设备 - private fun isAbDevice(): Boolean { - val abUpdate = runCommandGetOutput("getprop ro.build.ab_update") - if (!abUpdate.toBoolean()) return false - - val slotSuffix = runCommandGetOutput("getprop ro.boot.slot_suffix") - return slotSuffix.isNotEmpty() - } - - private fun cleanup() { - runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete") - runCommand(false, "rm -rf $workDir") - } - - private fun copy() { - uri?.let { safeUri -> - context.contentResolver.openInputStream(safeUri)?.use { input -> - FileOutputStream(File(filePath)).use { output -> - input.copyTo(output) - } - } - } - } - - private fun getBinary() { - runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}") - if (!File(binaryPath).exists()) { - throw IOException("Failed to extract update-binary") - } - } - - @SuppressLint("StringFormatInvalid") - private fun patch() { - val kernelVersion = runCommandGetOutput("cat /proc/version") - val versionRegex = """\d+\.\d+\.\d+""".toRegex() - val version = kernelVersion.let { versionRegex.find(it) }?.value ?: "" - val toolName = if (version.isNotEmpty()) { - val parts = version.split('.') - if (parts.size >= 2) { - val major = parts[0].toIntOrNull() ?: 0 - val minor = parts[1].toIntOrNull() ?: 0 - if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+" - } else { - "5_15+" - } - } else { - "5_15+" - } - val toolPath = "${context.filesDir.absolutePath}/mkbootfs" - AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath) - state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}") - runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath") - } - - private fun flash() { - val process = ProcessBuilder("su") - .redirectErrorStream(true) - .start() - - try { - process.outputStream.bufferedWriter().use { writer -> - writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n") - - // 写入槽位信息到临时文件 - slot?.let { selectedSlot -> - writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n") - } - - // 构建刷写命令 - val flashCommand = buildString { - append("sh $binaryPath 3 1 \"$filePath\"") - if (slot != null) { - append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"") - } - append(" && touch ${context.filesDir.absolutePath}/done\n") - } - - writer.write(flashCommand) - writer.write("exit\n") - writer.flush() - } - - process.inputStream.bufferedReader().use { reader -> - reader.lineSequence().forEach { line -> - if (line.startsWith("ui_print")) { - val logMessage = line.removePrefix("ui_print").trim() - state.addLog(logMessage) - - when { - logMessage.contains("extracting", ignoreCase = true) -> { - state.updateProgress(0.75f) - } - logMessage.contains("installing", ignoreCase = true) -> { - state.updateProgress(0.85f) - } - logMessage.contains("complete", ignoreCase = true) -> { - state.updateProgress(0.95f) - } - } - } - } - } - } finally { - process.destroy() - } - - if (!File("${context.filesDir.absolutePath}/done").exists()) { - throw IOException(context.getString(R.string.flash_failed_message)) - } - } - - private fun runCommand(su: Boolean, cmd: String): Int { - val shell = if (su) "su" else "sh" - val process = Runtime.getRuntime().exec(arrayOf(shell, "-c", cmd)) - - return try { - process.waitFor() - } finally { - process.destroy() - } - } - - private fun runCommandGetOutput(cmd: String): String { - return Shell.cmd(cmd).exec().out.joinToString("\n").trim() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt deleted file mode 100644 index 1540d3a6..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt +++ /dev/null @@ -1,757 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings - -import android.annotation.SuppressLint -import android.content.Context -import android.net.Uri -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.getValue -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.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.component.ImageEditorDialog -import com.sukisu.ultra.ui.component.KsuIsValid -import com.sukisu.ultra.ui.screen.SwitchItem -import com.sukisu.ultra.ui.theme.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import zako.zako.zako.zakoui.screen.moreSettings.component.ColorCircle -import zako.zako.zako.zakoui.screen.moreSettings.component.LanguageSelectionDialog -import zako.zako.zako.zakoui.screen.moreSettings.component.MoreSettingsDialogs -import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem -import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard -import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsDivider -import zako.zako.zako.zakoui.screen.moreSettings.component.SwitchSettingItem -import zako.zako.zako.zakoui.screen.moreSettings.component.UidScannerSection -import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState -import kotlin.math.roundToInt - -@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt") -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun MoreSettingsScreen( - navigator: DestinationsNavigator -) { - // 顶部滚动行为 - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } - val systemIsDark = isSystemInDarkTheme() - - // 创建设置状态管理器 - val settingsState = remember { MoreSettingsState(context, prefs, systemIsDark) } - val settingsHandlers = remember { MoreSettingsHandlers(context, prefs, settingsState) } - - // 图片选择器 - val pickImageLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - settingsState.selectedImageUri = it - settingsState.showImageEditor = true - } - } - - // 初始化设置 - LaunchedEffect(Unit) { - settingsHandlers.initializeSettings() - } - - // 显示图片编辑对话框 - if (settingsState.showImageEditor && settingsState.selectedImageUri != null) { - ImageEditorDialog( - imageUri = settingsState.selectedImageUri!!, - onDismiss = { - settingsState.showImageEditor = false - settingsState.selectedImageUri = null - }, - onConfirm = { transformedUri -> - settingsHandlers.handleCustomBackground(transformedUri) - settingsState.showImageEditor = false - settingsState.selectedImageUri = null - } - ) - } - - // 各种设置对话框 - MoreSettingsDialogs( - state = settingsState, - handlers = settingsHandlers - ) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.more_settings), - style = MaterialTheme.typography.titleLarge - ) - }, - navigationIcon = { - IconButton(onClick = { navigator.popBackStack() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) - }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) { - // 外观设置 - AppearanceSettings( - state = settingsState, - handlers = settingsHandlers, - pickImageLauncher = pickImageLauncher, - coroutineScope = coroutineScope - ) - - // 自定义设置 - CustomizationSettings( - state = settingsState, - handlers = settingsHandlers - ) - - // 高级设置 - KsuIsValid { - AdvancedSettings( - state = settingsState, - handlers = settingsHandlers - ) - } - } - } -} - -@Composable -private fun AppearanceSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers, - pickImageLauncher: ActivityResultLauncher, - coroutineScope: CoroutineScope -) { - SettingsCard(title = stringResource(R.string.appearance_settings)) { - // 语言设置 - LanguageSetting(state = state) - - // 主题模式 - SettingItem( - icon = Icons.Default.DarkMode, - title = stringResource(R.string.theme_mode), - subtitle = state.themeOptions[state.themeMode], - onClick = { state.showThemeModeDialog = true } - ) - - // 动态颜色开关 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SwitchSettingItem( - icon = Icons.Filled.ColorLens, - title = stringResource(R.string.dynamic_color_title), - summary = stringResource(R.string.dynamic_color_summary), - checked = state.useDynamicColor, - onChange = handlers::handleDynamicColorChange - ) - } - - // 主题色选择 - AnimatedVisibility( - visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !state.useDynamicColor, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - ThemeColorSelection(state = state) - } - - SettingsDivider() - - // DPI 设置 - DpiSettings(state = state, handlers = handlers) - - SettingsDivider() - - // 自定义背景设置 - CustomBackgroundSettings( - state = state, - handlers = handlers, - pickImageLauncher = pickImageLauncher, - coroutineScope = coroutineScope - ) - } -} - -@Composable -private fun CustomizationSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - SettingsCard(title = stringResource(R.string.custom_settings)) { - // 图标切换 - SwitchSettingItem( - icon = Icons.Default.Android, - title = stringResource(R.string.icon_switch_title), - summary = stringResource(R.string.icon_switch_summary), - checked = state.useAltIcon, - onChange = handlers::handleIconChange - ) - - // 显示更多模块信息 - SwitchSettingItem( - icon = Icons.Filled.Info, - title = stringResource(R.string.show_more_module_info), - summary = stringResource(R.string.show_more_module_info_summary), - checked = state.showMoreModuleInfo, - onChange = handlers::handleShowMoreModuleInfoChange - ) - - // 简洁模式开关 - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.simple_mode), - summary = stringResource(R.string.simple_mode_summary), - checked = state.isSimpleMode, - onChange = handlers::handleSimpleModeChange - ) - - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.kernel_simple_kernel), - summary = stringResource(R.string.kernel_simple_kernel_summary), - checked = state.isKernelSimpleMode, - onChange = handlers::handleKernelSimpleModeChange - ) - - // 各种隐藏选项 - HideOptionsSettings(state = state, handlers = handlers) - } -} - -@Composable -private fun HideOptionsSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - // 隐藏内核版本号 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_kernel_kernelsu_version), - summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), - checked = state.isHideVersion, - onChange = handlers::handleHideVersionChange - ) - - // 隐藏模块数量等信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_other_info), - summary = stringResource(R.string.hide_other_info_summary), - checked = state.isHideOtherInfo, - onChange = handlers::handleHideOtherInfoChange - ) - - // SuSFS 状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_susfs_status), - summary = stringResource(R.string.hide_susfs_status_summary), - checked = state.isHideSusfsStatus, - onChange = handlers::handleHideSusfsStatusChange - ) - - // Zygisk 实现状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_zygisk_implement), - summary = stringResource(R.string.hide_zygisk_implement_summary), - checked = state.isHideZygiskImplement, - onChange = handlers::handleHideZygiskImplementChange - ) - - if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.show_kpm_info), - summary = stringResource(R.string.show_kpm_info_summary), - checked = state.isShowKpmInfo, - onChange = handlers::handleShowKpmInfoChange - ) - } - - // 隐藏链接信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_link_card), - summary = stringResource(R.string.hide_link_card_summary), - checked = state.isHideLinkCard, - onChange = handlers::handleHideLinkCardChange - ) - - // 隐藏标签行 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_tag_card), - summary = stringResource(R.string.hide_tag_card_summary), - checked = state.isHideTagRow, - onChange = handlers::handleHideTagRowChange - ) -} - -@Composable -private fun AdvancedSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val snackBarHost = remember { SnackbarHostState() } - val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } - - SettingsCard(title = stringResource(R.string.advanced_settings)) { - // SELinux 开关 - SwitchSettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.selinux), - summary = if (state.selinuxEnabled) - stringResource(R.string.selinux_enabled) else - stringResource(R.string.selinux_disabled), - checked = state.selinuxEnabled, - onChange = handlers::handleSelinuxChange - ) - - var forceSignatureVerification by rememberSaveable { - mutableStateOf(prefs.getBoolean("force_signature_verification", false)) - } - - // 强制签名验证开关 - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.module_signature_verification), - summary = stringResource(R.string.module_signature_verification_summary), - checked = forceSignatureVerification, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("force_signature_verification", enabled) } - forceSignatureVerification = enabled - } - ) - - // UID 扫描开关 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { - UidScannerSection(prefs, snackBarHost, scope, context) - } - - // 动态管理器设置 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { - SettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.dynamic_manager_title), - subtitle = if (state.isDynamicSignEnabled) { - stringResource(R.string.dynamic_manager_enabled_summary, state.dynamicSignSize) - } else { - stringResource(R.string.dynamic_manager_disabled) - }, - onClick = { state.showDynamicSignDialog = true } - ) - } - } -} - -@Composable -private fun ThemeColorSelection(state: MoreSettingsState) { - SettingItem( - icon = Icons.Default.Palette, - title = stringResource(R.string.theme_color), - subtitle = when (ThemeConfig.currentTheme) { - is ThemeColors.Green -> stringResource(R.string.color_green) - is ThemeColors.Purple -> stringResource(R.string.color_purple) - is ThemeColors.Orange -> stringResource(R.string.color_orange) - is ThemeColors.Pink -> stringResource(R.string.color_pink) - is ThemeColors.Gray -> stringResource(R.string.color_gray) - is ThemeColors.Yellow -> stringResource(R.string.color_yellow) - else -> stringResource(R.string.color_default) - }, - onClick = { state.showThemeColorDialog = true }, - trailingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 8.dp) - ) { - val theme = ThemeConfig.currentTheme - val isDark = isSystemInDarkTheme() - - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - ) -} - -@Composable -private fun DpiSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - SettingItem( - icon = Icons.Default.FormatSize, - title = stringResource(R.string.app_dpi_title), - subtitle = stringResource(R.string.app_dpi_summary), - onClick = {}, - trailingContent = { - Text( - text = handlers.getDpiFriendlyName(state.tempDpi), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - ) - - // DPI 滑动条和控制 - DpiSliderControls(state = state, handlers = handlers) -} - -@Composable -private fun DpiSliderControls( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - val sliderValue by animateFloatAsState( - targetValue = state.tempDpi.toFloat(), - label = "DPI Slider Animation" - ) - - Slider( - value = sliderValue, - onValueChange = { newValue -> - state.tempDpi = newValue.toInt() - state.isDpiCustom = !state.dpiPresets.containsValue(state.tempDpi) - }, - valueRange = 160f..600f, - steps = 11, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - - // DPI 预设按钮行 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - ) { - state.dpiPresets.forEach { (name, dpi) -> - val isSelected = state.tempDpi == dpi - val buttonColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - MaterialTheme.colorScheme.surfaceVariant - - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 2.dp) - .clip(RoundedCornerShape(8.dp)) - .background(buttonColor) - .clickable { - state.tempDpi = dpi - state.isDpiCustom = false - } - .padding(vertical = 8.dp, horizontal = 4.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = name, - style = MaterialTheme.typography.labelMedium, - color = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - Text( - text = if (state.isDpiCustom) - "${stringResource(R.string.dpi_size_custom)}: ${state.tempDpi}" - else - "${handlers.getDpiFriendlyName(state.tempDpi)}: ${state.tempDpi}", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 8.dp) - ) - - Button( - onClick = { state.showDpiConfirmDialog = true }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - enabled = state.tempDpi != state.currentDpi - ) { - Icon( - Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.dpi_apply_settings)) - } - } -} - -@Composable -private fun CustomBackgroundSettings( - state: MoreSettingsState, - handlers: MoreSettingsHandlers, - pickImageLauncher: ActivityResultLauncher, - coroutineScope: CoroutineScope -) { - // 自定义背景开关 - SwitchSettingItem( - icon = Icons.Filled.Wallpaper, - title = stringResource(id = R.string.settings_custom_background), - summary = stringResource(id = R.string.settings_custom_background_summary), - checked = state.isCustomBackgroundEnabled, - onChange = { isChecked -> - if (isChecked) { - pickImageLauncher.launch("image/*") - } else { - handlers.handleRemoveCustomBackground() - } - } - ) - - // 透明度和亮度调节 - AnimatedVisibility( - visible = ThemeConfig.customBackgroundUri != null, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically() - ) { - BackgroundAdjustmentControls( - state = state, - handlers = handlers, - coroutineScope = coroutineScope - ) - } -} - -@Composable -private fun BackgroundAdjustmentControls( - state: MoreSettingsState, - handlers: MoreSettingsHandlers, - coroutineScope: CoroutineScope -) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - // 透明度滑动条 - AlphaSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) - - // 亮度调节滑动条 - DimSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) - } -} - -@Composable -private fun AlphaSlider( - state: MoreSettingsState, - handlers: MoreSettingsHandlers, - coroutineScope: CoroutineScope -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 4.dp) - ) { - Icon( - Icons.Filled.Opacity, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_alpha), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(state.cardAlpha * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val alphaSliderValue by animateFloatAsState( - targetValue = state.cardAlpha, - label = "Alpha Slider Animation" - ) - - Slider( - value = alphaSliderValue, - onValueChange = { newValue -> - handlers.handleCardAlphaChange(newValue) - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(handlers.context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) -} - -@Composable -private fun DimSlider( - state: MoreSettingsState, - handlers: MoreSettingsHandlers, - coroutineScope: CoroutineScope -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) - ) { - Icon( - Icons.Filled.LightMode, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_dim), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(state.cardDim * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val dimSliderValue by animateFloatAsState( - targetValue = state.cardDim, - label = "Dim Slider Animation" - ) - - Slider( - value = dimSliderValue, - onValueChange = { newValue -> - handlers.handleCardDimChange(newValue) - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(handlers.context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) -} - -fun saveCardConfig(context: Context) { - CardConfig.save(context) -} - -@Composable -private fun LanguageSetting(state: MoreSettingsState) { - val context = LocalContext.current - val language = stringResource(id = R.string.settings_language) - - // Compute display name based on current app locale - val currentLanguageDisplay = remember(state.currentAppLocale) { - val locale = state.currentAppLocale - if (locale != null) { - locale.getDisplayName(locale) - } else { - context.getString(R.string.language_system_default) - } - } - - SettingItem( - icon = Icons.Filled.Translate, - title = language, - subtitle = currentLanguageDisplay, - onClick = { state.showLanguageDialog = true } - ) - - // Language Selection Dialog - if (state.showLanguageDialog) { - LanguageSelectionDialog( - onLanguageSelected = { newLocale -> - // Update local state immediately - state.currentAppLocale = LocaleHelper.getCurrentAppLocale(context) - // Apply locale change immediately for Android < 13 - LocaleHelper.restartActivity(context) - }, - onDismiss = { state.showLanguageDialog = false } - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt deleted file mode 100644 index b5b9921a..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt +++ /dev/null @@ -1,459 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.res.Configuration -import android.net.Uri -import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CleaningServices -import androidx.compose.material.icons.filled.Groups -import androidx.compose.material.icons.filled.Scanner -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.ConfirmResult -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.screen.SettingItem -import com.sukisu.ultra.ui.screen.SwitchItem -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.util.* -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState -import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon - -/** - * 更多设置处理器 - */ -class MoreSettingsHandlers( - val context: Context, - private val prefs: SharedPreferences, - private val state: MoreSettingsState -) { - - /** - * 初始化设置 - */ - fun initializeSettings() { - // 加载设置 - CardConfig.load(context) - state.cardAlpha = CardConfig.cardAlpha - state.cardDim = CardConfig.cardDim - state.isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null - - // 设置主题模式 - state.themeMode = when (ThemeConfig.forceDarkMode) { - true -> 2 - false -> 1 - null -> 0 - } - - // 确保卡片样式跟随主题模式 - when (state.themeMode) { - 2 -> { // 深色 - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - } - 1 -> { // 浅色 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = true - } - 0 -> { // 跟随系统 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = false - } - } - - // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 - if (state.themeMode == 0 && state.systemIsDark) { - CardConfig.setThemeDefaults(true) - } - - state.currentDpi = prefs.getInt("app_dpi", state.systemDpi) - state.tempDpi = state.currentDpi - - CardConfig.save(context) - - // 初始化 SELinux 状态 - state.selinuxEnabled = Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing" - - // 初始化动态管理器配置 - state.dynamicSignConfig = Natives.getDynamicManager() - state.dynamicSignConfig?.let { config -> - if (config.isValid()) { - state.isDynamicSignEnabled = true - state.dynamicSignSize = config.size.toString() - state.dynamicSignHash = config.hash - } - } - } - - /** - * 处理主题模式变更 - */ - fun handleThemeModeChange(index: Int) { - state.themeMode = index - val newThemeMode = when (index) { - 0 -> null // 跟随系统 - 1 -> false // 浅色 - 2 -> true // 深色 - else -> null - } - context.saveThemeMode(newThemeMode) - ThemeConfig.updateTheme(darkMode = newThemeMode) - - when (index) { - 2 -> { // 深色 - ThemeConfig.updateTheme(darkMode = true) - CardConfig.updateThemePreference(darkMode = true, lightMode = false) - CardConfig.setThemeDefaults(true) - CardConfig.save(context) - } - 1 -> { // 浅色 - ThemeConfig.updateTheme(darkMode = false) - CardConfig.updateThemePreference(darkMode = false, lightMode = true) - CardConfig.setThemeDefaults(false) - CardConfig.save(context) - } - 0 -> { // 跟随系统 - ThemeConfig.updateTheme(darkMode = null) - CardConfig.updateThemePreference(darkMode = null, lightMode = null) - val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - CardConfig.setThemeDefaults(isNightModeActive) - CardConfig.save(context) - } - } - } - - /** - * 处理主题色变更 - */ - fun handleThemeColorChange(theme: ThemeColors) { - context.saveThemeColors(when (theme) { - ThemeColors.Green -> "green" - ThemeColors.Purple -> "purple" - ThemeColors.Orange -> "orange" - ThemeColors.Pink -> "pink" - ThemeColors.Gray -> "gray" - ThemeColors.Yellow -> "yellow" - else -> "default" - }) - ThemeConfig.updateTheme(theme = theme) - } - - /** - * 处理动态颜色变更 - */ - fun handleDynamicColorChange(enabled: Boolean) { - state.useDynamicColor = enabled - context.saveDynamicColorState(enabled) - ThemeConfig.updateTheme(dynamicColor = enabled) - } - - /** - * 获取DPI大小友好名称 - */ - @Composable - fun getDpiFriendlyName(dpi: Int): String { - return when (dpi) { - 240 -> stringResource(R.string.dpi_size_small) - 320 -> stringResource(R.string.dpi_size_medium) - 420 -> stringResource(R.string.dpi_size_large) - 560 -> stringResource(R.string.dpi_size_extra_large) - else -> stringResource(R.string.dpi_size_custom) - } - } - - /** - * 应用 DPI 设置 - */ - fun handleDpiApply() { - if (state.tempDpi != state.currentDpi) { - prefs.edit { - putInt("app_dpi", state.tempDpi) - } - - state.currentDpi = state.tempDpi - Toast.makeText( - context, - context.getString(R.string.dpi_applied_success, state.tempDpi), - Toast.LENGTH_SHORT - ).show() - - val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(restartIntent) - - state.showDpiConfirmDialog = false - } - } - - /** - * 处理自定义背景 - */ - fun handleCustomBackground(transformedUri: Uri) { - context.saveAndApplyCustomBackground(transformedUri) - state.isCustomBackgroundEnabled = true - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true - saveCardConfig(context) - - Toast.makeText( - context, - context.getString(R.string.background_set_success), - Toast.LENGTH_SHORT - ).show() - } - - /** - * 处理移除自定义背景 - */ - fun handleRemoveCustomBackground() { - context.saveCustomBackground(null) - state.isCustomBackgroundEnabled = false - CardConfig.cardAlpha = 1f - CardConfig.cardDim = 0f - CardConfig.isCustomAlphaSet = false - CardConfig.isCustomDimSet = false - CardConfig.isCustomBackgroundEnabled = false - saveCardConfig(context) - ThemeConfig.preventBackgroundRefresh = false - - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { - putBoolean("prevent_background_refresh", false) - } - - Toast.makeText( - context, - context.getString(R.string.background_removed), - Toast.LENGTH_SHORT - ).show() - } - - /** - * 处理卡片透明度变更 - */ - fun handleCardAlphaChange(newValue: Float) { - state.cardAlpha = newValue - CardConfig.cardAlpha = newValue - CardConfig.isCustomAlphaSet = true - prefs.edit { - putBoolean("is_custom_alpha_set", true) - putFloat("card_alpha", newValue) - } - } - - /** - * 处理卡片亮度变更 - */ - fun handleCardDimChange(newValue: Float) { - state.cardDim = newValue - CardConfig.cardDim = newValue - CardConfig.isCustomDimSet = true - prefs.edit { - putBoolean("is_custom_dim_set", true) - putFloat("card_dim", newValue) - } - } - - /** - * 处理图标变更 - */ - fun handleIconChange(newValue: Boolean) { - prefs.edit { putBoolean("use_alt_icon", newValue) } - state.useAltIcon = newValue - toggleLauncherIcon(context, newValue) - Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show() - } - - /** - * 处理简洁模式变更 - */ - fun handleSimpleModeChange(newValue: Boolean) { - prefs.edit { putBoolean("is_simple_mode", newValue) } - state.isSimpleMode = newValue - } - - /** - * 处理内核简洁模式变更 - */ - fun handleKernelSimpleModeChange(newValue: Boolean) { - prefs.edit { putBoolean("is_kernel_simple_mode", newValue) } - state.isKernelSimpleMode = newValue - } - - /** - * 处理隐藏版本变更 - */ - fun handleHideVersionChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_version", newValue) } - state.isHideVersion = newValue - } - - /** - * 处理隐藏其他信息变更 - */ - fun handleHideOtherInfoChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_other_info", newValue) } - state.isHideOtherInfo = newValue - } - - /** - * 处理显示KPM信息变更 - */ - fun handleShowKpmInfoChange(newValue: Boolean) { - prefs.edit { putBoolean("show_kpm_info", newValue) } - state.isShowKpmInfo = newValue - } - - /** - * 处理隐藏SuSFS状态变更 - */ - fun handleHideSusfsStatusChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_susfs_status", newValue) } - state.isHideSusfsStatus = newValue - } - - /** - * 处理隐藏Zygisk实现变更 - */ - fun handleHideZygiskImplementChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) } - state.isHideZygiskImplement = newValue - } - - /** - * 处理隐藏链接卡片变更 - */ - fun handleHideLinkCardChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_link_card", newValue) } - state.isHideLinkCard = newValue - } - - /** - * 处理隐藏标签行变更 - */ - fun handleHideTagRowChange(newValue: Boolean) { - prefs.edit { putBoolean("is_hide_tag_row", newValue) } - state.isHideTagRow = newValue - } - - /** - * 处理显示更多模块信息变更 - */ - fun handleShowMoreModuleInfoChange(newValue: Boolean) { - prefs.edit { putBoolean("show_more_module_info", newValue) } - state.showMoreModuleInfo = newValue - } - - /** - * 处理SELinux变更 - */ - fun handleSelinuxChange(enabled: Boolean) { - val command = if (enabled) "setenforce 1" else "setenforce 0" - Shell.getShell().newJob().add(command).exec().let { result -> - if (result.isSuccess) { - state.selinuxEnabled = enabled - val message = if (enabled) - context.getString(R.string.selinux_enabled_toast) - else - context.getString(R.string.selinux_disabled_toast) - - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } else { - Toast.makeText( - context, - context.getString(R.string.selinux_change_failed), - Toast.LENGTH_SHORT - ).show() - } - } - } - - /** - * 处理动态管理器配置 - */ - fun handleDynamicManagerConfig(enabled: Boolean, size: String, hash: String) { - if (enabled) { - val parsedSize = parseDynamicSignSize(size) - if (parsedSize != null && parsedSize > 0 && hash.length == 64) { - val success = Natives.setDynamicManager(parsedSize, hash) - if (success) { - state.dynamicSignConfig = Natives.DynamicManagerConfig(parsedSize, hash) - state.isDynamicSignEnabled = true - state.dynamicSignSize = size - state.dynamicSignHash = hash - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_failed), - Toast.LENGTH_SHORT - ).show() - } - } else { - Toast.makeText( - context, - context.getString(R.string.invalid_sign_config), - Toast.LENGTH_SHORT - ).show() - } - } else { - val success = Natives.clearDynamicManager() - if (success) { - state.dynamicSignConfig = null - state.isDynamicSignEnabled = false - state.dynamicSignSize = "" - state.dynamicSignHash = "" - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_disabled_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_clear_failed), - Toast.LENGTH_SHORT - ).show() - } - } - } - - /** - * 解析动态签名大小 - */ - private fun parseDynamicSignSize(input: String): Int? { - return try { - when { - input.startsWith("0x", true) -> input.substring(2).toInt(16) - else -> input.toInt() - } - } catch (_: NumberFormatException) { - null - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt deleted file mode 100644 index 3c182c1f..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt +++ /dev/null @@ -1,201 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings.component - -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.NavigateNext -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.sukisu.ultra.ui.theme.* - -private val SETTINGS_GROUP_SPACING = 16.dp - -@Composable -fun SettingsCard( - title: String, - icon: ImageVector? = null, - content: @Composable () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = SETTINGS_GROUP_SPACING), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = getCardElevation(), - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp) - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - } - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - ) - } - content() - } - } -} - -@Composable -fun SettingItem( - icon: ImageVector, - title: String, - subtitle: String? = null, - onClick: () -> Unit, - iconTint: Color = MaterialTheme.colorScheme.primary, - trailingContent: @Composable (() -> Unit)? = { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 5.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = iconTint, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - if (subtitle != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - } - } - - trailingContent?.invoke() - } -} - -@Composable -fun SwitchSettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - onChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onChange(!checked) } - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - lineHeight = 20.sp, - ) - if (summary != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp, - ) - } - } - - Switch( - checked = checked, - onCheckedChange = onChange - ) - } -} - -@Composable -fun SettingsDivider() { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -fun ColorCircle( - color: Color, - isSelected: Boolean, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .size(20.dp) - .clip(CircleShape) - .background(color) - .then( - if (isSelected) { - Modifier.border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = CircleShape - ) - } else { - Modifier - } - ) - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt deleted file mode 100644 index cebfd8bb..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt +++ /dev/null @@ -1,620 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings.component - -import android.content.Context -import android.content.SharedPreferences -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.CleaningServices -import androidx.compose.material.icons.filled.Groups -import androidx.compose.material.icons.filled.Scanner -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -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.sukisu.ultra.Natives -import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.ConfirmResult -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.screen.SwitchItem -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment -import com.sukisu.ultra.ui.util.getUidMultiUserScan -import com.sukisu.ultra.ui.util.readUidScannerFile -import com.sukisu.ultra.ui.util.setUidAutoScan -import com.sukisu.ultra.ui.util.setUidMultiUserScan -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers -import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState - -@Composable -fun MoreSettingsDialogs( - state: MoreSettingsState, - handlers: MoreSettingsHandlers -) { - // 主题模式选择对话框 - if (state.showThemeModeDialog) { - SingleChoiceDialog( - title = stringResource(R.string.theme_mode), - options = state.themeOptions, - selectedIndex = state.themeMode, - onOptionSelected = { index -> - handlers.handleThemeModeChange(index) - }, - onDismiss = { state.showThemeModeDialog = false } - ) - } - - // DPI 设置确认对话框 - if (state.showDpiConfirmDialog) { - ConfirmDialog( - title = stringResource(R.string.dpi_confirm_title), - message = stringResource(R.string.dpi_confirm_message, state.currentDpi, state.tempDpi), - summaryText = stringResource(R.string.dpi_confirm_summary), - confirmText = stringResource(R.string.confirm), - dismissText = stringResource(R.string.cancel), - onConfirm = { handlers.handleDpiApply() }, - onDismiss = { - state.showDpiConfirmDialog = false - state.tempDpi = state.currentDpi - } - ) - } - - // 主题色选择对话框 - if (state.showThemeColorDialog) { - ThemeColorDialog( - onColorSelected = { theme -> - handlers.handleThemeColorChange(theme) - state.showThemeColorDialog = false - }, - onDismiss = { state.showThemeColorDialog = false } - ) - } - - // 动态管理器配置对话框 - if (state.showDynamicSignDialog) { - DynamicManagerDialog( - state = state, - onConfirm = { enabled, size, hash -> - handlers.handleDynamicManagerConfig(enabled, size, hash) - state.showDynamicSignDialog = false - }, - onDismiss = { state.showDynamicSignDialog = false } - ) - } -} - -@Composable -fun SingleChoiceDialog( - title: String, - options: List, - selectedIndex: Int, - onOptionSelected: (Int) -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - options.forEachIndexed { index, option -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onOptionSelected(index) - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedIndex == index, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(option) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} - -@Composable -fun ConfirmDialog( - title: String, - message: String, - summaryText: String? = null, - confirmText: String = stringResource(R.string.confirm), - dismissText: String = stringResource(R.string.cancel), - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column { - Text(message) - if (summaryText != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - summaryText, - style = MaterialTheme.typography.bodySmall - ) - } - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(confirmText) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(dismissText) - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LanguageSelectionDialog( - onLanguageSelected: (String) -> Unit, - onDismiss: () -> Unit -) { - val context = LocalContext.current - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - - // Check if should use system language settings - if (LocaleHelper.useSystemLanguageSettings) { - // Android 13+ - Jump to system settings - LocaleHelper.launchSystemLanguageSettings(context) - onDismiss() - } else { - // Android < 13 - Show app language selector - // Dynamically detect supported locales from resources - val supportedLocales = remember { - val locales = mutableListOf() - - // Add system default first - locales.add(java.util.Locale.ROOT) // This will represent "System Default" - - // Dynamically detect available locales by checking resource directories - val resourceDirs = listOf( - "ar", "bg", "de", "fa", "fr", "hu", "in", "it", - "ja", "ko", "pl", "pt-rBR", "ru", "th", "tr", - "uk", "vi", "zh-rCN", "zh-rTW" - ) - - resourceDirs.forEach { dir -> - try { - val locale = when { - dir.contains("-r") -> { - val parts = dir.split("-r") - java.util.Locale.Builder() - .setLanguage(parts[0]) - .setRegion(parts[1]) - .build() - } - else -> java.util.Locale.Builder() - .setLanguage(dir) - .build() - } - - // Test if this locale has translated resources - val config = android.content.res.Configuration() - config.setLocale(locale) - val localizedContext = context.createConfigurationContext(config) - - // Try to get a translated string to verify the locale is supported - val testString = localizedContext.getString(R.string.settings_language) - val defaultString = context.getString(R.string.settings_language) - - // If the string is different or it's English, it's supported - if (testString != defaultString || locale.language == "en") { - locales.add(locale) - } - } catch (_: Exception) { - // Skip unsupported locales - } - } - - // Sort by display name - val sortedLocales = locales.drop(1).sortedBy { it.getDisplayName(it) } - mutableListOf().apply { - add(locales.first()) // System default first - addAll(sortedLocales) - } - } - - val allOptions = supportedLocales.map { locale -> - val tag = if (locale == java.util.Locale.ROOT) { - "system" - } else if (locale.country.isEmpty()) { - locale.language - } else { - "${locale.language}_${locale.country}" - } - - val displayName = if (locale == java.util.Locale.ROOT) { - context.getString(R.string.language_system_default) - } else { - locale.getDisplayName(locale) - } - - tag to displayName - } - - val currentLocale = prefs.getString("app_locale", "system") ?: "system" - val options = allOptions.map { (tag, displayName) -> - ListOption( - titleText = displayName, - selected = currentLocale == tag - ) - } - - var selectedIndex by remember { - mutableIntStateOf(allOptions.indexOfFirst { (tag, _) -> currentLocale == tag }) - } - - ListDialog( - state = rememberUseCaseState( - visible = true, - onFinishedRequest = { - if (selectedIndex >= 0 && selectedIndex < allOptions.size) { - val newLocale = allOptions[selectedIndex].first - prefs.edit { putString("app_locale", newLocale) } - onLanguageSelected(newLocale) - } - onDismiss() - }, - onCloseRequest = { - onDismiss() - } - ), - header = Header.Default( - title = stringResource(R.string.settings_language), - ), - selection = ListSelection.Single( - showRadioButtons = true, - options = options - ) { index, _ -> - selectedIndex = index - } - ) - } -} -@Composable -fun ThemeColorDialog( - onColorSelected: (ThemeColors) -> Unit, - onDismiss: () -> Unit -) { - val themeColorOptions = listOf( - stringResource(R.string.color_default) to ThemeColors.Default, - stringResource(R.string.color_green) to ThemeColors.Green, - stringResource(R.string.color_purple) to ThemeColors.Purple, - stringResource(R.string.color_orange) to ThemeColors.Orange, - stringResource(R.string.color_pink) to ThemeColors.Pink, - stringResource(R.string.color_gray) to ThemeColors.Gray, - stringResource(R.string.color_yellow) to ThemeColors.Yellow - ) - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.choose_theme_color)) }, - text = { - Column { - themeColorOptions.forEach { (name, theme) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onColorSelected(theme) } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val isDark = isSystemInDarkTheme() - Box( - modifier = Modifier.padding(end = 12.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - Text(name) - Spacer(modifier = Modifier.weight(1f)) - // 当前选中的主题显示选中标记 - if (ThemeConfig.currentTheme::class == theme::class) { - Icon( - Icons.Default.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = onDismiss - ) { - Text(stringResource(R.string.cancel)) - } - } - ) -} - -@Composable -fun DynamicManagerDialog( - state: MoreSettingsState, - onConfirm: (Boolean, String, String) -> Unit, - onDismiss: () -> Unit -) { - var localEnabled by remember { mutableStateOf(state.isDynamicSignEnabled) } - var localSize by remember { mutableStateOf(state.dynamicSignSize) } - var localHash by remember { mutableStateOf(state.dynamicSignHash) } - - fun parseDynamicSignSize(input: String): Int? { - return try { - when { - input.startsWith("0x", true) -> input.substring(2).toInt(16) - else -> input.toInt() - } - } catch (_: NumberFormatException) { - null - } - } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.dynamic_manager_title)) }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - // 启用开关 - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { localEnabled = !localEnabled } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Switch( - checked = localEnabled, - onCheckedChange = { localEnabled = it } - ) - Spacer(modifier = Modifier.width(12.dp)) - Text(stringResource(R.string.enable_dynamic_manager)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 签名大小输入 - OutlinedTextField( - value = localSize, - onValueChange = { input -> - val isValid = when { - input.isEmpty() -> true - input.matches(Regex("^\\d+$")) -> true - input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true - else -> false - } - if (isValid) { - localSize = input - } - }, - label = { Text(stringResource(R.string.signature_size)) }, - enabled = localEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text - ) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - // 签名哈希输入 - OutlinedTextField( - value = localHash, - onValueChange = { hash -> - if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { - localHash = hash - } - }, - label = { Text(stringResource(R.string.signature_hash)) }, - enabled = localEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - supportingText = { - Text(stringResource(R.string.hash_must_be_64_chars)) - }, - isError = localEnabled && localHash.isNotEmpty() && localHash.length != 64 - ) - } - }, - confirmButton = { - Button( - onClick = { onConfirm(localEnabled, localSize, localHash) }, - enabled = if (localEnabled) { - parseDynamicSignSize(localSize)?.let { it > 0 } == true && - localHash.length == 64 - } else true - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} - -@Composable -fun UidScannerSection( - prefs: SharedPreferences, - snackBarHost: SnackbarHostState, - scope: CoroutineScope, - context: Context -) { - if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return - - val realAuto = Natives.isUidScannerEnabled() - val realMulti = getUidMultiUserScan() - - var autoOn by remember { mutableStateOf(realAuto) } - var multiOn by remember { mutableStateOf(realMulti) } - - LaunchedEffect(Unit) { - autoOn = realAuto - multiOn = realMulti - prefs.edit { - putBoolean("uid_auto_scan", autoOn) - putBoolean("uid_multi_user_scan", multiOn) - } - } - - SwitchItem( - icon = Icons.Filled.Scanner, - title = stringResource(R.string.uid_auto_scan_title), - summary = stringResource(R.string.uid_auto_scan_summary), - checked = autoOn, - onCheckedChange = { target -> - autoOn = target - if (!target) multiOn = false - - scope.launch(Dispatchers.IO) { - setUidAutoScan(target) - val actual = Natives.isUidScannerEnabled() || readUidScannerFile() - withContext(Dispatchers.Main) { - autoOn = actual - if (!actual) multiOn = false - prefs.edit { - putBoolean("uid_auto_scan", actual) - putBoolean("uid_multi_user_scan", multiOn) - } - if (actual != target) { - snackBarHost.showSnackbar( - context.getString(R.string.uid_scanner_setting_failed) - ) - } - } - } - } - ) - - AnimatedVisibility( - visible = autoOn, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SwitchItem( - icon = Icons.Filled.Groups, - title = stringResource(R.string.uid_multi_user_scan_title), - summary = stringResource(R.string.uid_multi_user_scan_summary), - checked = multiOn, - onCheckedChange = { target -> - scope.launch(Dispatchers.IO) { - val ok = setUidMultiUserScan(target) - withContext(Dispatchers.Main) { - if (ok) { - multiOn = target - prefs.edit { putBoolean("uid_multi_user_scan", target) } - } else { - snackBarHost.showSnackbar( - context.getString(R.string.uid_scanner_setting_failed) - ) - } - } - } - } - ) - } - - AnimatedVisibility( - visible = autoOn, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - val confirmDialog = rememberConfirmDialog() - com.sukisu.ultra.ui.screen.SettingItem( - icon = Icons.Filled.CleaningServices, - title = stringResource(R.string.clean_runtime_environment), - summary = stringResource(R.string.clean_runtime_environment_summary), - onClick = { - scope.launch { - if (confirmDialog.awaitConfirm( - title = context.getString(R.string.clean_runtime_environment), - content = context.getString(R.string.clean_runtime_environment_confirm) - ) == ConfirmResult.Confirmed - ) { - if (cleanRuntimeEnvironment()) { - autoOn = false - multiOn = false - prefs.edit { - putBoolean("uid_auto_scan", false) - putBoolean("uid_multi_user_scan", false) - } - Natives.setUidScannerEnabled(false) - snackBarHost.showSnackbar( - context.getString(R.string.clean_runtime_environment_success) - ) - } else { - snackBarHost.showSnackbar( - context.getString(R.string.clean_runtime_environment_failed) - ) - } - } - } - } - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt deleted file mode 100644 index 26e9593c..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt +++ /dev/null @@ -1,101 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings.state - -import android.content.Context -import android.content.SharedPreferences -import android.net.Uri -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.ThemeConfig - -/** - * 更多设置状态管理 - */ -@Stable -class MoreSettingsState( - val context: Context, - val prefs: SharedPreferences, - val systemIsDark: Boolean -) { - // 主题模式选择 - var themeMode by mutableIntStateOf( - when (ThemeConfig.forceDarkMode) { - true -> 2 // 深色 - false -> 1 // 浅色 - null -> 0 // 跟随系统 - } - ) - - // 动态颜色开关状态 - var useDynamicColor by mutableStateOf(ThemeConfig.useDynamicColor) - - // 语言设置 - var showLanguageDialog by mutableStateOf(false) - var currentAppLocale by mutableStateOf(LocaleHelper.getCurrentAppLocale(context)) - - // 对话框显示状态 - var showThemeModeDialog by mutableStateOf(false) - var showThemeColorDialog by mutableStateOf(false) - var showDpiConfirmDialog by mutableStateOf(false) - var showImageEditor by mutableStateOf(false) - - // 动态管理器配置状态 - var dynamicSignConfig by mutableStateOf(null) - var isDynamicSignEnabled by mutableStateOf(false) - var dynamicSignSize by mutableStateOf("") - var dynamicSignHash by mutableStateOf("") - var showDynamicSignDialog by mutableStateOf(false) - - - // 各种设置开关状态 - var isSimpleMode by mutableStateOf(prefs.getBoolean("is_simple_mode", false)) - var isHideVersion by mutableStateOf(prefs.getBoolean("is_hide_version", false)) - var isHideOtherInfo by mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) - var isShowKpmInfo by mutableStateOf(prefs.getBoolean("show_kpm_info", false)) - var isHideZygiskImplement by mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false)) - var isHideSusfsStatus by mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) - var isHideLinkCard by mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) - var isHideTagRow by mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) - var isKernelSimpleMode by mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false)) - var showMoreModuleInfo by mutableStateOf(prefs.getBoolean("show_more_module_info", false)) - var useAltIcon by mutableStateOf(prefs.getBoolean("use_alt_icon", false)) - - // SELinux状态 - var selinuxEnabled by mutableStateOf(false) - - // 卡片配置状态 - var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha) - var cardDim by mutableFloatStateOf(CardConfig.cardDim) - var isCustomBackgroundEnabled by mutableStateOf(ThemeConfig.customBackgroundUri != null) - - // 图片选择状态 - var selectedImageUri by mutableStateOf(null) - - // DPI 设置 - val systemDpi = context.resources.displayMetrics.densityDpi - var currentDpi by mutableIntStateOf(prefs.getInt("app_dpi", systemDpi)) - var tempDpi by mutableIntStateOf(currentDpi) - var isDpiCustom by mutableStateOf(true) - - // 主题模式选项 - val themeOptions = listOf( - context.getString(R.string.theme_follow_system), - context.getString(R.string.theme_light), - context.getString(R.string.theme_dark) - ) - - // 预设 DPI 选项 - val dpiPresets = mapOf( - context.getString(R.string.dpi_size_small) to 240, - context.getString(R.string.dpi_size_medium) to 320, - context.getString(R.string.dpi_size_large) to 420, - context.getString(R.string.dpi_size_extra_large) to 560 - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt deleted file mode 100644 index f383ec99..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt +++ /dev/null @@ -1,154 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings.util - -import android.annotation.SuppressLint -import android.annotation.TargetApi -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.net.Uri -import android.os.Build -import android.provider.Settings -import java.util.* - -object LocaleHelper { - - /** - * Check if should use system language settings (Android 13+) - */ - val useSystemLanguageSettings: Boolean - get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - - /** - * Launch system app locale settings (Android 13+) - */ - fun launchSystemLanguageSettings(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - try { - val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - } - context.startActivity(intent) - } catch (_: Exception) { - // Fallback to app language settings if system settings not available - } - } - } - - /** - * Apply saved language setting to context (for Android < 13) - */ - fun applyLanguage(context: Context): Context { - // On Android 13+, language is handled by system - if (useSystemLanguageSettings) { - return context - } - - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val localeTag = prefs.getString("app_locale", "system") ?: "system" - - return if (localeTag == "system") { - context - } else { - val locale = parseLocaleTag(localeTag) - setLocale(context, locale) - } - } - - /** - * Set locale for context (Android < 13) - */ - @SuppressLint("ObsoleteSdkInt") - private fun setLocale(context: Context, locale: Locale): Context { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - updateResources(context, locale) - } else { - updateResourcesLegacy(context, locale) - } - } - - @SuppressLint("UseRequiresApi", "ObsoleteSdkInt") - @TargetApi(Build.VERSION_CODES.N) - private fun updateResources(context: Context, locale: Locale): Context { - val configuration = Configuration() - configuration.setLocale(locale) - configuration.setLayoutDirection(locale) - return context.createConfigurationContext(configuration) - } - - @Suppress("DEPRECATION") - @SuppressWarnings("deprecation") - private fun updateResourcesLegacy(context: Context, locale: Locale): Context { - Locale.setDefault(locale) - val resources = context.resources - val configuration = resources.configuration - configuration.locale = locale - configuration.setLayoutDirection(locale) - resources.updateConfiguration(configuration, resources.displayMetrics) - return context - } - - /** - * Parse locale tag to Locale object - */ - private fun parseLocaleTag(tag: String): Locale { - return try { - if (tag.contains("_")) { - val parts = tag.split("_") - Locale.Builder() - .setLanguage(parts[0]) - .setRegion(parts.getOrNull(1) ?: "") - .build() - } else { - Locale.Builder() - .setLanguage(tag) - .build() - } - } catch (_: Exception) { - Locale.getDefault() - } - } - - /** - * Restart activity to apply language change (Android < 13) - */ - fun restartActivity(context: Context) { - if (context is Activity && !useSystemLanguageSettings) { - context.recreate() - } - } - - /** - * Get current app locale - */ - @SuppressLint("ObsoleteSdkInt") - fun getCurrentAppLocale(context: Context): Locale? { - return if (useSystemLanguageSettings) { - // Android 13+ - get from system app locale settings - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - try { - val localeManager = context.getSystemService(Context.LOCALE_SERVICE) as? android.app.LocaleManager - val locales = localeManager?.applicationLocales - if (locales != null && !locales.isEmpty) { - locales.get(0) - } else { - null // System default - } - } catch (_: Exception) { - null // System default - } - } else { - null // System default - } - } else { - // Android < 13 - get from SharedPreferences - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val localeTag = prefs.getString("app_locale", "system") ?: "system" - if (localeTag == "system") { - null // System default - } else { - parseLocaleTag(localeTag) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt deleted file mode 100644 index 88245055..00000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package zako.zako.zako.zakoui.screen.moreSettings.util - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import com.sukisu.ultra.ui.MainActivity - -/** - * 刷新启动器图标 - */ -fun toggleLauncherIcon(context: Context, useAlt: Boolean) { - val pm = context.packageManager - val main = ComponentName(context, MainActivity::class.java.name) - val alias = ComponentName(context, "${MainActivity::class.java.name}Alias") - - pm.setComponentEnabledSetting( - if (useAlt) alias else main, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ) - - pm.setComponentEnabledSetting( - if (useAlt) main else alias, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP - ) -} \ No newline at end of file diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 542a2566..5049e8d8 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -4,24 +4,24 @@ غير مثبت إضغط للتثبيت يعمل - الإصدار: %s + الإصدار: %d + مستخدمين الجذر: %d + الإضافات: %d غير مدعوم KernelSU يدعم GKI kernels فقط إصدار النواة - إصدار SuSFS إصدار المدير + البصمة وضع SELinux معطل مفروض متساهل مجهول مستخدم خارق - لا يمكن تشغيل %s الوحدة + فشل في تمكين الوحدة %s فشل تعطيل الإضافة : %s لا توجد إضافات مثبتة الإضافات - فرز (الإجراء أولاً) - فرز (الممكن أولاً) إلغاء التثبيت تثبيت الوحدة تثبيت @@ -38,6 +38,7 @@ فشل إلغاء تثبيت %s الإصدار المطور + الوحدات غير متوفرة حيث يتم تعطيل نظام الملفات المتراكب بواسطة النواة! إنعاش إظهار تطبيقات النظام إخفاء تطبيقات النظام @@ -49,316 +50,92 @@ https://kernelsu.org/guide/what-is-kernelsu.html تعرف على كيفية تثبيت KernelSU واستخدام الإضافات إدعمنا - KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك أن تظهر لنا أنك تهتم بالتبرع. + KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك متى ما استطعت أن تظهر لنا أنك تهتم بالتبرع. إنضم إلى قناتنا في %2$s ]]> - الإفتراضي - نموذج - مُخصّص - اسم الملف الشخصي - مجموعات القدرات - سياق SELinux - الغاء تحميل الإضافات - فشل تحديث ملف تعريف التطبيق لـ %s - إصدار KernelSU الحالي %s منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %s أو أعلى! - الغاء تحميل الإضافات بشكل افتراضي - القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. - سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. - المجال - القواعد تحديث تحميل الإضافة: %s ابدأ التنزيل: %s الإصدار الجديد: %s متاح ، انقر للتحديث. تشغيل - ايقاف إجباري + الإفتراضي + نموذج + موروث + عالمي + فردي + مجموعات + مُخصّص + تركيب مساحة الاسم + الغاء تحميل الإضافات + فشل تحديث ملف تعريف التطبيق لـ %s + سياق SELinux + ايقاف إجباري + الغاء تحميل الإضافات بشكل افتراضي + القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. + سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. + المجال + القواعد إعادة تشغيل التطبيق فشل تحديث قواعد SELinux لـ %s + اسم الملف الشخصي + إصدار KernelSU الحالي %d منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %d أو أعلى! سجل التغييرات - قالب ملف تعريف التطبيق - إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق - إنشاء قالب - تحرير القالب - المعرف - معرف القالب غير صالح - الاسم - الوصف - حفظ - حذف - عرض القالب - للقراءة فقط - معرف القالب موجود بالفعل! - استيراد / تصدير - استيراد من الحافظة + تم الاستيراد بنجاح تصدير إلى الحافظة لا يمكن العثور على القالب المحلي للتصدير! - تم الاستيراد بنجاح - مزامنة القوالب عبر الإنترنت - فشل في حفظ القالب - الحافظة فارغة! + معرف القالب موجود بالفعل! + استيراد من الحافظة فشل في جلب سجل التغيير: %s - التحقق من التحديث - التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق + الاسم + معرف القالب غير صالح + مزامنة القوالب عبر الإنترنت + إنشاء قالب + للقراءة فقط + استيراد / تصدير + فشل في حفظ القالب + تحرير القالب + المعرف + قالب ملف تعريف التطبيق + الوصف + حفظ + إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق + حذف + الحافظة فارغة! + عرض القالب فشل في منح صلاحية الجذر! - إجراء - إغلاق + فتح + التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق + التحقق من التحديث تمكين تصحيح أخطاء WebView يمكن استخدامه لتصحيح أخطاء WebUI، يرجى تمكينه فقط عند الحاجة. - تثبيت مباشر (موصى به) + التالي اختيار ملف + تثبيت مباشر (موصى به) التثبيت على فتحة غير نشطة (بعد OTA) سيتم **إجبار** جهازك على التمهيد إلى الفتحة غير النشطة الحالية بعد إعادة التشغيل! \nاستخدم هذا الخيار فقط بعد انتهاء التحديث. \nأستمرار؟ - التالي - يوصى باستخدام صورة القسم %1$s اختر KMI + يوصى باستخدام صورة القسم %1$s إلغاء التثبيت إلغاء التثبيت مؤقتًا إلغاء التثبيت بشكل دائم استعادة الصورة الاصلية - قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية. ‬إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم. - استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\". تركيب نجح التركيب فشل التركيب LKM المحددة: %s + استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\". + قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية. حفظ السجلات + إجراء السجلات محفوظة - - تأكيد وحدة التثبيت %1$s؟ - وحدة غير معروفة - - تأكيد استعادة الوحدة - هذه العملية سوف تستبدل جميع الوحدات الموجودة. هل تريد المتابعة؟ - تأكيد - إلغاء - - النسخ الاحتياطي ناجح (tar.gz) - فشل النسخ الاحتياطي: %1$s - وحدات النسخ الاحتياطي - استعادة الوحدات - - تم استعادة الوحدات بنجاح, إعادة التشغيل مطلوبة - فشل الاستعادة: %1$s - أعد تشغيل التطبيق الآن - خطأ غير معروف - - فشل تنفيذ الأوامر: %1$s - - السماح بالنسخ الاحتياطي للقائمة بنجاح - فشل النسخ الاحتياطي لقائمة السماح: %1$s - تأكيد استعادة القائمة المسموح بها - هذه العملية ستقوم بالكتابة فوق قائمة المسموح بها. هل تريد المتابعة؟ - تمت استعادة القائمة بنجاح - فشل استعادة القائمة المسموحة: %1$s - قائمة النسخ الاحتياطي - استعادة قائمة المسموح بها - خلفية التطبيق المخصصة - حدد صورة كخلفية - شفافية شريط التنقل - ‏إصدار Android - نوع الجهاز - لا يسمح بمنح المستخدم المتميز ل %s + فرز (الممكن أولاً) + فرز (الإجراء أولاً) تعطيل توافق su - تعطيل أي تطبيقات مؤقتًا من الحصول على امتيازات الجذر عن طريق الأمر <unk> su (لن تتأثر عمليات الجذر الحالية). - هل أنت متأكد من أنك تريد تثبيت وحدات %1$d التالية؟ \n\n%2$s - المزيد من الإعدادات - SELinux - مفعّل - رفض - وضع البساطة - إخفاء البطاقات غير الضرورية عند تشغيلها - إخفاء إصدار النواة - إخفاء إصدار النواة - إخفاء معلومات أخرى - يخفي معلومات عن عدد المستخدمين المتميزين والوحدات ووحدات KPM على الصفحة الرئيسية - إخفاء حالة SuSFS - إخفاء معلومات حالة SuSFS على الصفحة الرئيسية - إخفاء حالة بطاقة الرابط - إخفاء معلومات البطاقة في الصفحة الرئيسية - الثيم - اتبّاع النظام - فاتح - مظلم - ربط يدوي - لون ديناميكية - الألوان الديناميكية باستخدام سمات النظام - اختر لون السمة - أزرق - أخضر - أرجواني - برتقالي - وردي - رمادي - الأصفر - Anykernel3 yükle - فلاش AnyKernel3 ملف kernel - يتطلب امتيازات الجذر - اكتمل التشويش - هل تريد إعادة التشغيل فوراً؟ - نعم - لايوجد - فشل إعادة التشغيل - KPM - لا توجد وحدات نواة مثبتة في هذا الوقت - الإصدار - المؤلف - إلغاء التثبيت - تم إلغاء التثبيت بنجاح - فشل في إلغاء التثبيت - تم تحميل وحدة كيلو جزء بنجاح - فشل تحميل وحدة كيلو بايم - العوامل المتغيرة - تنفيذ - إصدار KPM - إغلاق - تم تطوير وظائف نواة الوحدة النمطية التالية بواسطة KernelPatch وتعديلها لتشمل وظائف نواة الوحدة النمطية لـ SukiSU Ultra - سوكيسو أولترا تتطلع إلى الأمام - نجحت - فشل - وستشكل سوكيسو أولترا في المستقبل فرعا مستقلا نسبيا من فروع الوحدة، ولكننا لا نزال نقدر كيرنيل سو وموكسو الرسميين وما إلى ذلك. لإسهاماتهم! - غير مدعوم - إدعمنا - النواة غير مصحوبة - لم يتم تكوين النواة - الإعدادات المُخصصة - KPM Install - التحميل - فسيفساء - الرجاء التحديد: %1\$s وضع تثبيت الوحدة \n\nالتحميل: قم بتحميل الوحدة \nمؤقتا: تثبيت دائم في النظام - غير قادر على التحقق من وجود ملف الوحدة - ألوان المظهر - نوع الملف غير صحيح! الرجاء تحديد ملف .kpm. - إلغاء التثبيت - سيتم إلغاء تثبيت KPM التالية: %s - استخدم إصبعين لتكبير الصورة، وأصبع واحد لسحبها لضبط الموضع - إعادة - - الفلاش اكتمل - - جار التحضير - تنظيف الملفات… - جارٍ نسخ الملف… - استخراج أداة فلاش… - تعديل البرنامج النصي الفلاش… - نواة رمادية… - الفلاش اكتمل - - حدد فتحة الفلاش - الرجاء تحديد الخانة المستهدفة للتشغيل المبطن - خانة A - الخانة B - الفتحة المحددة: %1$s - الحصول على الفتحة الأصلية - تعيين الفتحة المحددة - استعادة الخانة الافتراضية - فتحة النظام الحالية الافتراضية:%1$s - - فشل النسخ - خطأ غير معروف - فشل التركيب - - إصلاح/تثبيت LKM - نواة رمادية - إصدار النواة:%1$s - استخدام أداة التصحيح:%1$s - تعيين - إعدادات التطبيق - ادوات - - التطبيق غير موجود - تم تمكين SELinux - تم تعطيل SELinux - فشل تغيير حالة SELinux - إعدادات متقدمة - تخصيص شريط الأدوات - عد مرة أخرى - تم تعيين الخلفية بنجاح - إزالة خلفيات مخصصة - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - عرض وظيفة KPM - إخفاء معلومات KPM ووظيفتها في الشريط المنزلي والأسفل - - حدد محرك WebUI لاستخدامه - اختيار تلقائي - Force the use of WebUI X - Mandatory use of KSU WebUI - حقن Eruda في WebUI X - حقن وحدة التصحيح في WebUI X لجعل تصحيح الأخطاء أسهل. يتطلب تصحيح أخطاء الويب لتكون قيد التشغيل. - - تم تطبيق DPI - ضبط كثافة عرض الشاشة للتطبيق الحالي فقط - صغير - متوسط - كبير - حجم كبير - قابلة للتعديل - تطبيق إعدادات DPI - تأكيد تغيير إدارة شؤون الإعلام - هل أنت متأكد من أنك تريد تغيير تطبيق DPI من %1$d إلى %2$d؟ - يحتاج التطبيق إلى إعادة تشغيل لتطبيق الإعدادات الجديدة لإدارة شؤون الإعلام، ولا يؤثر على شريط حالة النظام أو التطبيقات الأخرى - تم تعيين DPI إلى %1$d، فعلي بعد إعادة تشغيل التطبيق - - لغة التطبيق - اتبع النظام - تعديل ظلام البطاقة - - رمز الخطأ - يرجى التحقق من السجل - تم تثبيت الوحدة %1$d/%2$d - أخفق %d في تثبيت وحدة جديدة - فشل تحميل الوحدة - ضرب النواة - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - في الأعلى - أسفل - محدد - خيار - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + قم بتعطيل قدره التطبيقات مؤقتا من الحصول على امتيازات المسخدم الخارق عبر الأمر su (لن تتأثر عمليات الجذر الحالية). + الحزم الاتيه سيتم تثبيتها %1$s + تأكيد + من الغير ممكن اعطاء صلاحيات (المسخدم الخارق) لـ %s diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml index d90028db..86518745 100644 --- a/manager/app/src/main/res/values-az/strings.xml +++ b/manager/app/src/main/res/values-az/strings.xml @@ -1,362 +1,102 @@ Ana səhifə + Super istifadəçilər: %d + Nüvə Yüklənmədi Yükləmək üçün toxunun İşləyir - Versiya: %s - Dəstəklənmir + Versiya: %d + Modullar: %d Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir - Nüvə - SuSFS Version - Menecer versiyası - SELinux vəziyyəti - Qeyri-aktiv - Məcburi - Sərbəst + Dəstəklənmir + Yüklə + Yüklə Naməlum + Barmaq izi + Menecer versiyası + Qeyri-aktiv + SELinux vəziyyəti + Sərbəst + Məcburi Super istifadəçi + Sil Modulu aktiv etmək mümkün olmadı: %s Modulu deaktiv etmək mümkün olmadı: %s Heç bir modul quraşdırılmayıb Modul - Sort (Action first) - Sort (Enabled first) - Sil - Yüklə - Yüklə Yenidən başlat Parametrlər - Yüngül vəziyyətdə yenodən başlat Bərpa rejimində yenidən başlat + Yüngül vəziyyətdə yenodən başlat Bootloader rejimində yenidən başlat Yükləmə rejimində yenidən başlat - EDL rejimində yenidən başlat - Haqqında - Modulu silmək istədiyinizdən əminsiniz %s\? - %s silindi - Silmək mümkün olmadı: %s Versiya Sahib - Yenilə + Modulu silmək istədiyinizdən əminsiniz %s\? Sistem proqramlarını göstər + Haqqında + EDL rejimində yenidən başlat + Silmək mümkün olmadı: %s + %s silindi Sistem proqramlarını gizlət + overlayfs mövcud deyil,modul işləyə bilməyəcək! Log-u göndər + Yenilə Təhlükəsiz rejimi Qüvvəyə minməsi üçün yenidən başlat Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur! KernelSU-yu öyrən https://kernelsu.org/guide/what-is-kernelsu.html - KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən Bizi dəstəkləyin - KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz. - Join our %2$s channel]]> - Defolt + KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən Şablon + Defolt Özəl + KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz. + Mənbə kodlarımıza baxın %1$s
Kanalımıza %2$s qoşulun
Profil adı - Qruplar Bacarıqlar - SELinux konteksi Modulları umount et - %s görə tətbiq profillərini güncəlləmək mümkün olmadı - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! + Miras qalmış + Qlobal + Bölmənin ad sahəsi + Fərdi + Qruplar Defolt olaraq modulları umount et + SELinux konteksi + %s görə tətbiq profillərini güncəlləmək mümkün olmadı Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək. - Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək. Domen Qaydalar Güncəllə - Modul yüklənir: %s Endirməni başlat: %s Yeni versiya: %s əlçatandır, endirmək üçün toxunun + Modul yüklənir: %s + Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək. - Məcburi dayandır - Yenidən başlat + Məcburi dayandır + Təkrar başlat %s görə SELinux qaydalarını güncəlləmək mümkün olmadı - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s Girişləri Saxla - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Cari KernelSU versiyası %d menecerin düzgün işləməsi üçün çox aşağıdır. Lütfən, %d və ya daha yüksək versiyaya təkmilləşdirin! + Aşağıdakı modullar quraşdırılacaq: %1$s + Təsdiq edin + Sıralama (ilk hərəkət) + Sıralama (Əvvəlcə aktivdir) + Dəyişikliklər jurnalı + Tətbiq Profil Şablonu + Tətbiq Profilinə aid yerli və onlayn şablonların idarə olunması + Şablon yarat + Şablonu redaktə et + ID + Etibarsız şablon ID-si + Ad + Açıqlama + %s üçün Superistifadəçi girişi vermək mümkün olmadı. + Yadda saxla + Sil + Şablonu göstər + Yalnız oxu + Şablon ID-si artıq mövcuddur!
diff --git a/manager/app/src/main/res/values-bn-rBD/strings.xml b/manager/app/src/main/res/values-bn-rBD/strings.xml index e77f90cd..9d8ddff7 100644 --- a/manager/app/src/main/res/values-bn-rBD/strings.xml +++ b/manager/app/src/main/res/values-bn-rBD/strings.xml @@ -6,9 +6,11 @@ মোডিউল ইনেবল করা যায়নি: %s ইন্সটল করটে চাপুন কাজ করছে + মোডিউল: %d অমূলক কর্নেল ম্যানেজার ভারসন + ফিঙ্গারপ্রিন্ট ডিসেবল এনফোর্সিং সুপার ইউজার @@ -19,6 +21,7 @@ রিবুট সেটিংস সফট রিবুট + গ্লোবাল গ্রুপস এসইলিনাক্স কন্টেক্সট %s এর জন্য অ্যাপ প্রফাইল আপডেট করা যায়নি @@ -28,7 +31,11 @@ পারমিসিভ মোডিউল ডিসেবল করা যায়নি: %s কোনো মোডিউল ইন্সটল করা নেই - সংস্করণ: %s + সংস্করণ: %d + সুপার ইউজার: %d + নেইম স্পেস মাউন্ট + ইনহেরিটেড + ইন্ডিভিজুয়াল ক্যাপাবিলিটিস আনমাউন্ট মোডিউলস রিকভারিতে বুট diff --git a/manager/app/src/main/res/values-bn/strings.xml b/manager/app/src/main/res/values-bn/strings.xml index 8d59f0ae..398b2ade 100644 --- a/manager/app/src/main/res/values-bn/strings.xml +++ b/manager/app/src/main/res/values-bn/strings.xml @@ -4,11 +4,14 @@ ইনস্টল করা হয়নি ইনস্টল করার জন্য ক্লিক করুন ওয়ার্কিং - ওয়ার্কিং সংস্করণ: %s + ওয়ার্কিং সংস্করণ: %d + সুপার ইউজার: %d + মডিউল: %d অসমর্থিত KernelSU শুধুমাত্র GKI কার্নেল সমর্থন করে কার্নেল ম্যানেজার সংস্করণ + ফিঙ্গারপ্রিন্ট SELinux স্টেটাস ডিজেবল কার্যকর @@ -35,10 +38,11 @@ আনইন্সটল ব্যর্থ: %s ভার্সন লেখক + ওভারলেএফএস উপলব্ধ নয়, মডিউল কাজ করতে পারে না! রিফ্রেশ শো সিস্টেম অ্যাপস হাইড সিস্টেম অ্যাপস - সেন্ড লগ + সেন্ড লগ সেইফ মোড রিবুট এপ্লাই মডিউলগুলি অক্ষম কারণ তারা ম্যাজিস্কের সাথে বিরোধিতা করে! @@ -47,13 +51,18 @@ কিভাবে কার্নেলএসইউ ইনস্টল করতে হয় এবং মডিউল ব্যবহার করতে হয় তা শিখুন সাপোর্ট টাইটেল কার্নেলএসইউ বিনামূল্যে এবং ওপেন সোর্স, এবং সবসময় থাকবে। আপনি সবসময় একটি অনুদান দিয়ে আপনার কৃতজ্ঞতা প্রদর্শন করতে পারেন. + আমাদের %2$s চ্যানেল মার্জ করুন]]> প্রফাইলের নাম + নেমস্পেস মাউন্ট গ্রুপস যোগ্যতা এসই লিনাক্স কনটেক্সট ডিফল্ট টেমপ্লেট কাস্টম + গ্লোবাল + আলাদাভাবে আনমাউন্ট মোডিউল + ম্যানেজার সঠিকভাবে কাজ করার জন্য বর্তমান KernelSU সংস্করণ %d খুবই কম। অনুগ্রহ করে %d বা উচ্চতর সংস্করণে আপগ্রেড করুন! লগ সংরক্ষণ করুন diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml index da5e8299..688e47f2 100644 --- a/manager/app/src/main/res/values-bs/strings.xml +++ b/manager/app/src/main/res/values-bs/strings.xml @@ -1,362 +1,86 @@ - Početna - Nije instalirano - Kliknite da instalirate - Radi - Verzija: %s - Nepodržano - KernelSU samo podržava GKI kernele sad - Kernel - SuSFS Version - Verzija Upravitelja - SELinux stanje - Isključeno - U Provođenju - Permisivno - Nepoznato - Superkorisnik - Neuspješno uključivanje module: %s - Neuspješno isključivanje module: %s - Nema instaliranih modula - Modula - Sort (Action first) - Sort (Enabled first) - Deinstalirajte - Instalirajte - Instalirajte - Ponovo pokrenite - Podešavanja - Lagano Ponovo pokretanje - Ponovo pokrenite u Oporavu - Ponovo pokrenite u Pogonski Učitavatelj - Ponovo pokrenite u Preuzimanje - Ponovo pokrenite u EDL - O - Jeste li sigurni da želite deinstalirati modulu %s\? - %s deinstalirana - Neuspješna deinstalacija: %s - Verzija - Autor - Osvježi - Prikažite sistemske aplikacije - Sakrijte sistemske aplikacije - Pošaljite Izvještaj - Sigurnosni mod - Ponovo pokrenite da bi proradilo - Module su isključene jer je u sukobu sa Magisk-om! - Naučite KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Naučite kako da instalirate KernelSU i da koristite module - Podržite Nas - KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. - Join our %2$s channel]]> - Zadano - Šablon - Prilagođeno - Naziv profila + Imenski prostor nosača + Naslijeđen + Globalan + Pojedinačan Grupe Sposobnosti SELinux kontekst Umount module Ažuriranje Profila Aplikacije za %s nije uspjelo - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! + Trenutna KernelSU verzija %d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %d ili noviju! Umount module po zadanom Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. - Domena - Pravila Ažuriranje Skidanje module: %s Započnite sa skidanjem: %s Nova verzija: %s je dostupna, kliknite da skinete Pokrenite - Prisilno Zaustavite + Prisilno Zaustavite Resetujte + U Provođenju + Početna + Nije instalirano + Kliknite da instalirate + Superkorisnici: %d + Module: %d + Nepodržano + KernelSU samo podržava GKI kernele sad + Verzija Upravitelja + Otisak prsta + SELinux stanje + Instalirajte + Instalirajte + Ponovo pokrenite + Podešavanja + Verzija + Autor + Osvježi + Prikažite sistemske aplikacije + Sakrijte sistemske aplikacije + Sigurnosni mod + Ponovo pokrenite da bi proradilo + "Moduli su nedostupni jer su u sukobu sa Magisk-om!" + https://kernelsu.org/guide/what-is-kernelsu.html + Naučite kako da instalirate KernelSU i da koristite module + Podržite Nas + Pošaljite Izvještaj + Naučite KernelSU + Pogledajte izvornu kodu na %1$s
Pridružite nam se na %2$s kanalu
+ Domena + Pravila Neuspješno ažuriranje SELinux pravila za: %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s + Radi + Verzija: %d + Kernel verzija + Permisivno + Deinstalirajte + Nepoznato + Nema instaliranih modula + Superkorisnik + Modula + Ponovo pokrenite u Pogonski Učitavatelj + Ponovo pokrenite u Oporavu + %s deinstalirana + Lagano Ponovo pokretanje + Neuspješno uključivanje module: %s + Ponovo pokrenite u Preuzimanje + Neuspješno isključivanje module: %s + Ponovo pokrenite u EDL + Neuspješna deinstalacija: %s + Isključeno + O + Jeste li sigurni da želite deinstalirati modulu %s\? + overlayFS je onemogućen od strane kernela, modul nije dostupan. + KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. + Zadano + Šablon + Prilagođeno + Naziv profila Sačuvaj Dnevnike - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Modul će biti instaliran + Sortiraj + Potvrdi
diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml index 3c3221a5..09095c6e 100644 --- a/manager/app/src/main/res/values-da/strings.xml +++ b/manager/app/src/main/res/values-da/strings.xml @@ -1,362 +1,154 @@ - Hjem - Ikke installeret - Klik for at installere Arbejder - Version: %s + Moduler: %d Ikke understøttet - KernelSU understøtter kun GKI kernels - Kernel - SuSFS Version - Manager Version + Kernel-version + KernelSU understøtter nu kun GKI-kerner. + Manager version SELinux-status Deaktiveret - Håndhævende Tilladende - Ukendt Superbruger - Aktivering af modul fejlede: %s + Håndhævende Deaktivering af modul fejlede: %s Intet modul installeret - Modul - Sort (Action first) - Sort (Enabled first) Afinstaller Installer Installer Genstart Indstillinger - Blød Genstart - Genstart til Recovery - Genstart til Bootloader + Blød genstart Genstart til Download Genstart til EDL Om Er du sikker på, at du vil afinstallere modulet %s\? %s afinstalleret Afinstallation af: %s fejlede - Version - Forfatter + Moduler utilgængelige - OverlayFS deaktiveret i kernen! Opdater - Vis system-apps - Gem system-apps - Send Log + Send logfiler Sikker tilstand Genstart for at tage effekt - Moduler er deaktiveret, fordi der er konflikt med Magiskes! Lær KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Lær hvordan man installerer KernelSU og moduler - Støt Os - KernelSU er, og vil altid være gratis og open source. Du kan stadig vise os din støtte ved at donere. + Lær hvordan man installerer KernelSU og moduler. Join our %2$s channel]]> Standard Skabelon - Brugerdefineret - Profilnavn + Monter namespace + Arvet + Global Grupper Evner SELinux-kontext Afmonteret moduler - Opdatering af App Profil for %s fejlede - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Afmontere moduler som standard - Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil. - Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation. - Domæne - Regler + Afmontere moduler + Hvis denne indstilling aktiveres, kan KernelSU gendanne alle ændrede filer af modulerne til denne app. Opdatering Downloader modulet: %s - Start download: %s - Ny version: %s er tilgængelig, kilk for at downloade + Ny version %s er tilgængelig, klik for at opgradere! Start - Tving Stop + Tving stop + Opdatering af SELinux-regler mislykkedes for %s + Start download: %s + Klik for at installere + Version: %d + Hjem + Ikke installeret + Superbrugere: %d + Fingeraftryk + Ukendt + Aktivering af modul fejlede: %s + Genstart til Recovery + Modul + Forfatter + Genstart til Bootloader + Version + Gem system-apps + Vis system-apps + Moduler er utilgængelige på grund af en konflikt med Magisk! + Støt Os + KernelSU er, og vil altid være, gratis og åben kildekode. Du kan dog vise os, at du holder af os, ved at give en donation. + Brugerdefineret + Profilnavn + Individuel + Opdatering af App Profil for %s fejlede + Den globale standardværdi for \"Umount moduler\" i App Profile. Hvis aktiveret, fjernes alle modulændringer til systemet for apps, der ikke har en profil angivet. + Domæne + Regler Genstart - Opdatering af SELinux-regler for: %s fejlede - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template + Den nuværende KernelSU-version %d er for lav til, at manageren fungerer korrekt. Opgrader venligst til version %d eller højere! + Gem logs + Følgende moduler installeres: %1$s + Sorter (Handling først) + Sorter (Aktiveret først) + Bekræft + Kunne ikke tildele superbruger-adgang til %s + Ændringslog + App-profilskabelon + Administrer lokale og online skabeloner til App-profil. + Opret skabelon + Rediger skabelon ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". + Ugyldigt skabelon-ID + Navn + Beskrivelse + Gem + Slet + Visningsskabelon + Skrivebeskyttet + Skabelon-ID findes allerede! + Import/Eksport + Importér fra udklipsholder + Eksporter til udklipsholder + Kan ikke finde lokal skabelon til eksport! + Importér med succes + Synkroniser online skabeloner + Kunne ikke gemme skabelon + Udklipsholderen er tom! + Hent changelog mislykkedes: %s + Check for opdateringer + Søg automatisk efter opdateringer, når appen åbnes. + Kunne ikke tildelle root! + Handling + Åbn + WebView-fejlsøgning + Kan bruges til at fejlfinde af WebUI. Aktiver kun når det er nødvendigt. + Direkte installation (Anbefalet) + Vælg en fil + Installer på inaktiv slot (efter OTA-opdatering) + Din enhed vil blive **TVUNGET** til at starte op fra det aktuelt inaktive slot efter en genstart!\nBrug kun denne mulighed, når OTA er færdig.\nFortsæt? + Næste + %1$s partitionsbillede anbefales + Vælg KMI + Afinstaller + Afinstaller midlertidigt + Afinstaller permanent + Gendan systemets standardbillede + Afinstaller KernelSU midlertidigt, og gendan til den oprindelige tilstand efter næste genstart. + Afinstallation af KernelSU (root og alle moduler) fuldstændigt og permanent. + Gendan det originale fabriksbillede (hvis en sikkerhedskopi eksisterer), hvilket normalt bruges før en OTA-opdatering; hvis du skal afinstallere KernelSU, skal du bruge \"Afinstaller permanent\". Flashing - Flash success - Flash failed - Selected LKM: %s - Gem Logfiler - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Flash-succes + Flash mislykkedes + Valgt LKM: %s + Logs gemt + Deaktiver su-kompatibilitet + Deaktiver muligheden for at apps kan få root-privilegier via su-kommandoen (eksisterende root-processer påvirkes ikke). + Søg efter modulopdateringer + Brug lokal KLM-fil + Kun .ko-filer understøttes + Deaktiver kerne umount + Deaktiver umount-adfærd på kerneniveau, der styres af KernelSU. + Aktivér forbedret sikkerhed + Aktivér strengere sikkerhedspolitikker. + Standard + Aktivér midlertidigt + Aktivér permanent + Behandler… + Træk ned for at opdatere + Slip for at opdatere + Opdaterer… + Opdateret diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml index b9d848a4..8bdad66b 100644 --- a/manager/app/src/main/res/values-de/strings.xml +++ b/manager/app/src/main/res/values-de/strings.xml @@ -2,363 +2,135 @@ Startseite Nicht installiert - Tippe zum Installieren - Funktioniert - Version: %s - Nicht unterstützt - KernelSU unterstützt derzeit nur GKI-Kernel - Kernel - SuSFS version - Manager-Version - SELinux Status - Deaktiviert - Erzwingen Permissiv - Unbekannt + Funktioniert + Version: %d Superuser - Modulaktivierung fehlgeschlagen: %s - Moduldeaktivierung fehlgeschlagen: %s - Keine Modul installiert - Modul - Sortiere zuerst (Aktion) - Sortieren (zuerst aktiviert) - Deinstallieren - Installieren - Installieren - Neustarten - Einstellungen - Soft-Reboot - In den Recovery-Modus neustarten + Tippe zum Installieren + Superuser: %d + Unbekannt + Erzwingen In den Bootloader-Modus neustarten In den Download-Modus neustarten In den EDL-Modus neustarten - Über KernelSU - Möchtest du wirklich Modul %s deinstallieren? - %s deinstalliert - Deinstallation fehlgeschlagen: %s - Version Autor - Aktualisieren - System-Apps anzeigen - System-Apps ausblenden - Protokoll senden - Sicherer Modus - Neustarten, damit Änderungen wirksam werden + Module sind nicht verfügbar, da OverlayFS vom Kernel deaktiviert ist. + Über KernelSU Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar! - KernelSU verstehen https://kernelsu.org/guide/what-is-kernelsu.html Erfahre, wie KernelSU installiert wird und wie Module verwendet werden Unterstütze uns KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst. - Begleiten Sie uns %2$s-Kanal]]> + SELinux-Kontext + Module standardmäßig aushängen + Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. Standard Vorlage Benutzerdefiniert + App-Profilaktualisierung für %s fehlgeschlagen + Geerbt + Global + Individuell + Domäne + Aktualisieren + Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. + Regeln + Starte Download: %s + Aktualisieren der SELinux-Regeln schlug fehl für: %s + Starten + Neue Version %s verfügbar, tippen zum Aktualisieren. + Stopp erzwingen + Neustarten + Module: %d + Manager-Version + SELinux Status + Deaktiviert + Modulaktivierung fehlgeschlagen: %s + Moduldeaktivierung fehlgeschlagen: %s + Keine Modul installiert + Modul + Deinstallieren + Installieren + Neustarten + Einstellungen + In den Recovery-Modus neustarten + %s deinstalliert + Version + Aktualisieren + System-Apps anzeigen + System-Apps ausblenden + Protokoll senden + KernelSU verstehen + Sicherer Modus + Neustarten, damit Änderungen wirksam werden + Unserem %2$s-Kanal beitreten]]> Profilname + Namespace einhängen Gruppen Fähigkeiten - SELinux-Kontext Module aushängen - App-Profilaktualisierung für %s fehlgeschlagen - Die aktuelle KernelSU-Version %s ist zu alt für diese Manager-Version. Bitte auf Version %s oder höher aktualisieren! - Module standardmäßig aushängen - Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. - Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. - Domäne - Regeln - Aktualisieren Lädt Modul %s herunter - Starte Download: %s - Neue Version %s verfügbar, tippen zum Aktualisieren. - Starten - Stopp erzwingen - Neustarten - Aktualisieren der SELinux-Regeln schlug fehl für: %s + Nicht unterstützt + KernelSU unterstützt derzeit nur GKI-Kernel + Kernel + Fingerabdruck + Installieren + Soft-Reboot + Möchtest du wirklich Modul %s deinstallieren? + Deinstallation fehlgeschlagen: %s + Die aktuelle KernelSU-Version %d ist zu alt für diese Manager-Version. Bitte auf Version %d oder höher aktualisieren! Änderungsprotokoll - App-Profil-Vorlage - Verwalte die lokale und online Vorlage des App-Profils - Vorlage erstellen - Vorlage bearbeiten - ID - Ungültige Vorlagen-ID - Name - Beschreibung - Speichern - Löschen - Vorlage ansehen - Schreibgeschützt - Vorlagen-ID existiert bereits! - Import/Export - Aus Zwischenablage importieren + Erfolgreich importiert In Zwischenablage exportieren Kann lokale Vorlage nicht finden! - Erfolgreich importiert - Online-Vorlagen synchronisieren - Schlug beim Speichern der Vorlage fehl - Zwischenablage ist leer! + Vorlagen-ID existiert bereits! + Aus Zwischenablage importieren Konnte Veränderungs-Protokoll nicht laden: %s - Auf Aktualisierung prüfen - Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird - Root-Zugriff konnte nicht gewährt werden! - Aktion - Schließen + Name + Ungültige Vorlagen-ID + Online-Vorlagen synchronisieren + Vorlage erstellen + Schreibgeschützt + Import/Export + Schlug beim Speichern der Vorlage fehl + Vorlage bearbeiten + ID + App-Profil-Vorlage + Beschreibung + Speichern + Verwalte die lokale und online Vorlage des App-Profils + Löschen + Zwischenablage ist leer! + Vorlage ansehen WebView-Debugging aktivieren Kann zum Fehlerbeheben der WebUI verwendet werden, bitte nur im Notfall aktivieren. + %1$s Partitionsabbild empfohlen + KMI auswählen + Weiter Direkte Installation (empfohlen) Datei auswählen In inaktiven Slot installieren (nach OTA) Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten! \nBenutze dies nur nach Fertigstellung des OTA. \nFortfahren? - Weiter - %1$s Partitionsabbild empfohlen - KMI auswählen - Deinstallieren + Root-Zugriff konnte nicht gewährt werden! + Öffnen + Auf Aktualisierung prüfen + Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird Temporär deinstallieren + Deinstallieren + KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren. + Protokolle Speichern Permanent deinstallieren Standard-Abbild wiederherstellen KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen. - KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren. Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\". Schreibt Schreiben erfolgreich Schreiben fehlgeschlagen Wähle LKM: %s - Protokolle Speichern + Aktion Protokolle gespeichert - - das Installationsmodul %1$s bestätigen ? - unbekannter Modul - - Modul-Wiederherstellung bestätigen - Diese Operation wird alle vorhandenen Module überschreiben. Fortfahren? + Folgende Module werden installiert: %1$s Bestätigen - Abbrechen - - Sicherung erfolgreich (tar.gz) - Sicherung fehlgeschlagen: %1$s - sicherungsmodule - wiederherstellen - - Module erfolgreich wiederhergestellt, Neustart erforderlich - Wiederherstellung fehlgeschlagen: %1$s - Jetzt Neustarten - Ein unbekannter Fehler ist aufgetreten - - Befehlsausführung fehlgeschlagen: %1$s - - Sicherung erfolgreich erlaubt - Sicherung der erlaubten Liste fehlgeschlagen: %1$s - Allowlist-Wiederherstellung bestätigen - Dieser Vorgang wird die aktuelle Berechtigungsliste überschreiben. Fortfahren? - Liste erfolgreich wiederhergestellt - Wiederherstellung der erlaubten Liste fehlgeschlagen: %1$s - Sicherungsliste - Allowlist wiederherstellen - Eigener App-Hintergrund - Wählen Sie ein Bild als Hintergrund - Transparenz der Navigationsleiste - Androidversion - Geräteausführung - Superuser %s zu erlauben ist nicht erlaubt - Su Kompatibilität deaktivieren - Deaktivieren Sie temporär alle Anwendungen, die root-Privilegien über den Befehl <unk> su zu erhalten (bestehende root-Prozesse werden nicht beeinflusst). - Möchten Sie die folgenden %1$d Module installieren? \n\n\n%2$s - Weitere Einstellungen - SELinux - Aktiviert - Deaktiviert - Einfachheit Modus - Versteckt unnötige Karten beim Einschalten - Kernel-Version ausblenden - Kernel-Version ausblenden - Andere Infos ausblenden - Versteckt Informationen über die Anzahl der Supernutzer, Module und KPM-Module auf der Startseite - SuSFS-Status ausblenden - SuSFS Statusinformationen auf der Startseite ausblenden - Link-Kartenstatus ausblenden - Link Karteninformationen auf der Startseite ausblenden - Thema - Systemkonform - Licht - Dunkel - Manueller Hook - Dynamische Farbe - Dynamische Farben mit System-Themes - Wähle eine Theme-Farbe - Blau - Grün - Lila - Orange - Pink - Grau - Gelb - Install Anykernel3 - Flash AnyKernel3 Kernel-Datei - Erfordert Root-Rechte - Scrubbing abgeschlossen - Ob sofort neu gestartet werden soll? - Ja - Nein - Neustart fehlgeschlagen - KPM - Keine installierten Kernelmodule - Version - Autor - Deinstallieren - Erfolgreich deinstalliert - Deinstallation fehlgeschlagen - Laden des kpm Moduls erfolgreich - Laden des kpm-Moduls fehlgeschlagen - Parameter - Ausführen - KPM-Version - Schließen - Die folgenden Kernel-Modulfunktionen wurden von KernelPatch entwickelt und so modifiziert, dass die Funktionen des Kernel-Moduls von SukiSU Ultra enthalten sind - SukiSU Ultra freut sich auf - Erfolgreich - Fehlgeschlagen - SukiSU Ultra wird in Zukunft ein relativ unabhängiger Zweig der KSU sein, aber wir schätzen immer noch die offiziellen KernelSU und MKSU usw. für ihre Beiträge! - Nicht unterstützt - Unterstützt: - Kernel nicht gepatcht - Kernel nicht konfiguriert - Eigene Einstellungen - KPM Install - Laden - Einbetten - Bitte wählen: %1\$s Modul-Installationsmodus \n\nLaden: Das Modul \ntemporär laden: Dauerhaft in das System installieren - Kann nicht überprüfen, ob die Moduldatei existiert - Themenfarbe - Falscher Dateityp! Bitte wählen Sie eine .kpm Datei. - Deinstallieren - Folgende KPM wird deinstalliert: %s - Verwende zwei Finger um das Bild zu vergrößern und einen Finger um die Position anzupassen - Rückzahlung - - Blitz abgeschlossen - - Vorbereiten… - Bereinigung von Dateien… - Kopiere Datei… - Entpacken des Flash-Tools… - Patcht Flash-Skript… - Flashen des Kernels… - Blitz abgeschlossen - - Wähle Flash Slot - Bitte wählen Sie den Ziel-Slot zum Blinken des Boots aus - Slot A - Steckplatz B - Wähle LKM: %1$s - Den ursprünglichen Slot erhalten - Setze den angegebenen Slot - Standard wiederherstellen - Aktueller Standard-Slot des Systems:%1$s - - Kopieren fehlgeschlagen - Ein unbekannter Fehler ist aufgetreten - Schreiben fehlgeschlagen - - LKM Reparatur/Installation - Flashen des Kernels… - Kernel - Benutze das Patchwerkzeug:%1$s - Konfigurieren - Anwendungs-Einstellungen - Tools - - Anwendung nicht gefunden - SELinux aktiviert - SELinux deaktiviert - SELinux Statusänderung fehlgeschlagen - Erweiterte Einstellungen - Passt die Symbolleiste an. - Comeback - Hintergrund erfolgreich gesetzt - Eigene Hintergründe entfernt - Alternatives Symbol - Ändere das Launcher-Symbol auf das KernelSU Icon. - Icon gewechselt - - KPM-Funktion anzeigen - Versteckt KPM-Informationen und Funktion in der Home- und Unterleiste - - Wähle die zu verwendende WebUI-Engine - Automatisch auswählen - Nutzung von WebUI X erzwingen - Pflichtanwendung von KSU WebUI - Eruda in WebUI X injizieren - Fügen Sie eine Debug-Konsole in WebUI X ein, um das Debuggen zu vereinfachen. Benötigt Debugging im WebUI X. - - Angewendeter DPI - Bildschirmanzahl nur für die aktuelle Anwendung anpassen - Klein - Mittel - Groß - übergröße - anpassbar - DPI-Einstellungen anwenden - DPI-Änderung bestätigen - Sind Sie sicher, dass Sie die Anwendung DPI von %1$d auf %2$d ändern möchten? - Die Anwendung muss neu gestartet werden, um die neuen DPI-Einstellungen zu übernehmen, hat keine Auswirkungen auf die System-Statusleiste oder andere Anwendungen - DPI wurde auf %1$dgesetzt, wirksam nach dem Neustart der Anwendung - - App Sprache - Folge Systemeinstellung - Kartenfinsternis Anpassung - - fehlercode - Bitte überprüfen Sie das Log - Modul wird installiert %1$d/%2$d - %d Fehler bei der Installation eines neuen Moduls - Modul-Download fehlgeschlagen - Kernel-Flashen - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Unten - Ausgewählt - variieren - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml index 451ee778..8d825095 100644 --- a/manager/app/src/main/res/values-es/strings.xml +++ b/manager/app/src/main/res/values-es/strings.xml @@ -4,13 +4,16 @@ No instalado Haz clic para instalar Funcionando - Versión: %s + Versión: %d + Superusuarios: %d + Módulos: %d Sin soporte KernelSU solo admite kernels GKI por ahora Versión del kernel - Versión SuSFS Versión del gestor + Huella del dispositivo Estado de SELinux + Desactivado Estricto Permisivo @@ -20,8 +23,6 @@ Error al desactivar el módulo: %s Ningún módulo instalado Módulo - Ordenar (Acción primero) - Ordenar (Activado primero) Desinstalar Instalar Instalar @@ -38,6 +39,7 @@ Fallo al desinstalar: %s Versión Autor + Los módulos no están disponibles ya que OverlayFS está desactivado por el kernel. Refrescar Mostrar aplicaciones del sistema Ocultar aplicaciones del sistema @@ -50,17 +52,20 @@ Aprende a instalar KernelSU y a utilizar módulos Apóyanos KernelSU es, y siempre será, gratuito y de código abierto. Sin embargo, puedes demostrarnos que te importamos haciendo una donación. - Únete a nuestro canal %2$s]]> + Ver código fuente en %1$s
Únete a nuestro canal %2$s
Predeterminado Plantilla Personalizado Nombre de perfil + Montaje del espacio de nombres + Heredado + Global + Individual Grupos Capacidades Contexto SELinux Desmontar módulos Error al actualizar el perfil de la aplicación para %s - La versión %s actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %s o superior! Desmontar módulos por defecto El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido. Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. @@ -71,292 +76,55 @@ Iniciar descarga: %s La nueva versión %s está disponible, haga clic para actualizar. Iniciar - Forzar detención + Forzar detención Reiniciar Error al actualizar las reglas SELinux para: %s + La versión %d actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %d o superior! Registro de cambios - Plantilla de perfil de aplicación - Gestionar la plantilla local y en línea de App Profile - Crear plantilla - Editar plantilla - ID - ID de plantilla no válida - Nombre - Descripción - Guardar - Eliminar - Ver plantilla - Sólo lectura - ¡El ID de plantilla ya existe! - Importar/Exportar - Importar desde el portapapeles + Importado con éxito Exportar al portapapeles ¡No se encuentra la plantilla local para exportar! - Importado con éxito - Sincronizar plantillas en línea - No se ha podido guardar la plantilla - ¡El portapapeles está vacío! + ¡El ID de plantilla ya existe! + Importar desde el portapapeles Fallo en la obtención del registro de cambios: %s - Comprobar actualización - Comprobación automática de actualizaciones al abrir la aplicación - ¡No se ha podido conceder el acceso root! - Aktion - Cancelar + Nombre + ID de plantilla no válida + Sincronizar plantillas en línea + Crear plantilla + Sólo lectura + Importar/Exportar + No se ha podido guardar la plantilla + Editar plantilla + ID + Plantilla de perfil de aplicación + Descripción + Guardar + Gestionar la plantilla local y en línea de App Profile + Eliminar + ¡El portapapeles está vacío! + Ver plantilla + Guardar registros Activar la depuración de WebView - Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario. - Instalación directa (Recomendada) - Seleccione un archivo - Instalar en ranura inactiva (Después de OTA) - ¡Su dispositivo será **FORZADO** a arrancar en la ranura inactiva actual después de un reinicio!\nUtilice esta opción sólo después de que la OTA se haya realizado.\n¿Continuar? - Siguiente Se recomienda la imagen de partición %1$s Selecciona KMI + Siguiente + Instalación directa (Recomendada) + ¡Su dispositivo será **FORZADO** a arrancar en la ranura inactiva actual después de un reinicio!\nUtilice esta opción sólo después de que la OTA se haya realizado.\n¿Continuar? Desinstalar - Desinstalar temporalmente - Desinstalar permanentemente Restaurar imagen de archivo Desinstalar temporalmente KernelSU, restaurar al estado original tras el siguiente reinicio. - Desinstalar KernelSU (Root y todos los módulos) completa y permanentemente. - Restaurar la imagen de fábrica stock (Si existe una copia de seguridad), por lo general se utiliza antes de OTA; si necesita desinstalar KernelSU, por favor, utilice \"Desinstalar permanentemente\". - Intermitencia - Éxito de Flash - Flash falló LKM seleccionado: %s - Guardar registros - Registro guardado - - ¿confirmar la instalación del módulo %1$s? - módulo desconocido - - Confirmar restauración del módulo - Esta operación sobrescribirá todos los módulos existentes. ¿Continuar? - Confirmar - Cancelar - - Copia de seguridad exitosa (tar.gz) - Copia de seguridad fallida: %1$s - módulos de respaldo - restaurar módulos - - Módulos restaurados con éxito, se requiere reiniciar - Restauración fallida: %1$s - Reiniciar ahora - Error desconocido - - Ejecución del comando fallida: %1$s - - Copia de seguridad correcta - Copia de seguridad de lista fallida: %1$s - Confirmar restauración de lista de permisos - Esta operación sobrescribirá la lista permitida actual. ¿Continuar? - Lista restaurada correctamente - Restauración de lista de permisos falló: %1$s - Copia de seguridad lista - Restaurar lista de permisos - Fondo de aplicación personalizado - Seleccionar una imagen como fondo - Transparencia de la barra de navegación - Versión de Android - Modelo del dispositivo - No se permite conceder superusuario a %s - Desactivar compatibilidad su - Deshabilita temporalmente cualquier aplicación para obtener privilegios de root a través del comando de \"it\" (los procesos de root existentes no se verán afectados). - ¿Seguro que quieres instalar los siguientes módulos %1$d ? \n\n%2$s - Opciones avanzadas - SELinux - Habilitado - Desactivado - Modo de simplicidad - Ocultar tarjetas innecesarias al encender - Ocultar versión del núcleo - Ocultar versión del núcleo - Ocultar otra información - Oculta información sobre el número de superusuarios, módulos y módulos KPM en la página de inicio - Ocultar estado SuSFS - Ocultar información de estado de SuSFS en la página de inicio - Ocultar el estado de la tarjeta de enlace - Ocultar información de la tarjeta de enlace en la página de inicio - Temas - Predeterminado del sistema - Claro - Oscuro - Gancho manual - Color dinámico - Colores dinámicos usando temas del sistema - Elegir un color de tema - Azul - Verde - Morado - Naranjo - Rosa - Gris - Amarillo - Install Anykernel3 - Flash archivo del kernel AnyKernel3 - Requiere privilegios de root - Desguace completo - ¿Reiniciar inmediatamente? - Si - No - Reinicio fallido - KPM - No hay módulos del núcleo instalados en este momento - Versión - Autor - Desinstalar - Desinstalado con éxito - Error al desinstalar - Carga exitosa del módulo kpm - Error al cargar el módulo kpm - Parámetros - Empezar - Versión de KPM - Cancelar - Las siguientes funciones del módulo del núcleo fueron desarrolladas por KernelPatch y modificadas para incluir las funciones del módulo del núcleo de SukiSU Ultra - SukiSU Ultra espera a - Correctamente realizado - Fallido - SukiSU Ultra será una rama relativamente independiente de KSU en el futuro, pero todavía apreciamos el KernelSU oficial y MKSU etc. ¡por sus contribuciones! - Sin soporte - Apoyado - Kernel no parcheado - Kernel no configurado - Ajustes personalizados - KPM Install - Cargar - Insertar - Por favor seleccione: %1\$s Modo de instalación del Módulo \n\nCarga: Cargar temporalmente el módulo \nInsertar: Instalar permanentemente en el sistema - No se puede comprobar si el archivo de módulo existe - Color del tema - ¡Tipo de archivo incorrecto! Por favor seleccione el archivo .kpm. - Desinstalar - El siguiente KPM será desinstalado: %s - Usa dos dedos para acercar la imagen, y un dedo para arrastrarla para ajustar la posición - Reaprovisionamiento - - Flashear completo - - Preparando… - Limpiando archivos… - Copiando archivos… - Extrayendo herramienta flash… - Parcheando script flash… - Flashear kernel… - Flash completado - - Seleccionar Ranura Flash - Por favor, seleccione la ranura de destino para flashear el arranque - Slot A - Slot B - Slot selectionada - Obteniendo la ranura original - Establecer la ranura especificada - Restaurar Ranura Predeterminada - Ranura predeterminada del sistema actual:%1$s - - Hubo un fallo al copiar - Error desconocido - Flash falló - - Reparación/instalación de LKM - Flashear kernel - Versión del kernel - Usando la herramienta de parches:%1$s - Configurar - Configuración de la Aplicación - Herramientas - - No se ha encontrado la solicitud - SELinux habilitado - SELinux desactivado - Error al cambiar el estado de SELinux - Configuraciones avanzadas - Personalizar la barra de herramientas. - Retorno - Fondo establecido correctamente - Eliminar fondo personalizado - Icono alternativo - Cambiar el icono del lanzador al icono de KernelSU. - Icono cambiado - - Mostrar función KPM - Oculta la información y función del KPM en la barra de inicio e inferior - - Seleccione el motor WebUI a usar - Selección automática - Forzar el uso de WebUI X - Uso obligatorio de KSU WebUI - Inyectar Eruda en WebUI X - Inyecta una consola de depuración en WebUI X para facilitar la depuración. Requiere que la depuración web esté encendida. - - DPI aplicado - Ajustar la densidad de pantalla para la aplicación actual - Pequeño - Medio - Original - sobretamaño - personalizable - Aplicando ajustes de DPI - Confirmar cambio DPI - ¿Estás seguro de que quieres cambiar el DPI de la aplicación de %1$d a %2$d? - La aplicación necesita reiniciarse para aplicar la nueva configuración DPI, no afecta a la barra de estado del sistema u otras aplicaciones - DPI ha sido establecido a %1$d, efectivo después de reiniciar la aplicación - - Idioma de la aplicación - Seguir sistema - Ajuste de oscuridad de tarjeta - - código de error - Por favor, compruebe el registro - Módulo instalado %1$d/%2$d - %d falló al instalar un nuevo módulo - La descarga del modelo falló - Parpadeo Kernel - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Arriba - Abajo - Seleccionados - opción - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Flash falló + Éxito de Flash + ¡No se ha podido conceder el acceso root! + Abrir + Seleccione un archivo + Instalar en ranura inactiva (Después de OTA) + Desinstalar temporalmente + Desinstalar permanentemente + Desinstalar KernelSU (Root y todos los módulos) completa y permanentemente. + Comprobar actualización + Comprobación automática de actualizaciones al abrir la aplicación + Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario. + Restaurar la imagen de fábrica stock (Si existe una copia de seguridad), por lo general se utiliza antes de OTA; si necesita desinstalar KernelSU, por favor, utilice \"Desinstalar permanentemente\". diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml index 7c8640aa..839a6320 100644 --- a/manager/app/src/main/res/values-et/strings.xml +++ b/manager/app/src/main/res/values-et/strings.xml @@ -1,120 +1,119 @@ - Kodu - Pole paigaldatud - Klõpsa paigaldamiseks Töötamine - Versioon: %s - Mittetoetatud - KernelSU toetab hetkel vaid GSI tuumasid + Versioon: %d + Mooduleid: %d Tuum - SuSFS Version Manageri versioon - SELinuxi olek - Keelatud - Jõustav + Sõrmejälg Lubav - Teadmata - Superkasutaja Mooduli lubamine ebaõnnestus: %s - Mooduli keelamine ebaõnnestus: %s Mooduleid pole paigaldatud - Moodul - Sort (Action first) - Sort (Enabled first) - Eemalda - Paigalda - Paigalda Taaskäivita - Seaded - Pehme taaskäivitus Taaskäivita taastusesse - Taaskäivita käivituslaadurisse - Taaskäivita allalaadimisrežiimi - Taaskäivita EDL-i - Teave Kas soovid kindlasti eemaldada mooduli %s? %s eemaldatud - Eemaldamine ebaõnnestus: %s - Versioon - Autor - Värskenda - Kuva süsteemirakendused - Peida süsteemirakendused Saada logid Turvarežiim Muudatuste rakendamiseks taaskäivita - Moodulid pole saadaval Magiski konflikti tõttu! Õpi KernelSUd https://kernelsu.org/guide/what-is-kernelsu.html - Õpi KernelSUd paigaldama ja mooduleid kasutama - Toeta meid - KernelSU on, ja alati jääb, tasuta ning avatud lähtekoodiga kättesaadavaks. Sellegipoolest võid sa näidata, et hoolid, ning teha annetuse. - Join our %2$s channel]]> Vaikimisi - Mall - Kohandatud - Profiili nimi - Grupid - Võimekused - SELinux kontekst + Haagi nimeruum Lahtihaagitud moodulid Rakenduseprofiili uuendamine %s jaoks ebaõnnestus - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Haagi moodulid vaikimisi lahti - Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. - Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. + Allalaadimise alustamine: %s + SELinux reeglite uuendamine ebaõnnestus: %s + Muuda malli + Rakenduseprofiili mall + ID + Vaid lugemiseks + Malli ID juba eksisteerib! + Ekspordi lõikelauale + Sünkrooni võrgumallid + Muudatuste logi hankimine ebaõnnestus: %s + Kodu + Klõpsa paigaldamiseks + Pole paigaldatud + Mittetoetatud + Superkasutajaid: %d + KernelSU toetab hetkel vaid GSI tuumasid + SELinuxi olek + Keelatud + Jõustav + Teadmata + Superkasutaja + Mooduli keelamine ebaõnnestus: %s + Moodul + Taaskäivita käivituslaadurisse + Eemalda + Paigalda + Teave + Paigalda + Seaded + Pehme taaskäivitus + Taaskäivita allalaadimisrežiimi + Taaskäivita EDL-i + Värskenda + Autor + Eemaldamine ebaõnnestus: %s + Versioon + Moodulid pole saadaval, kuna OverlayFS on kernelis keelatud. + Kuva süsteemirakendused + Peida süsteemirakendused + Moodulid pole saadaval Magiski konflikti tõttu! + Õpi KernelSUd paigaldama ja mooduleid kasutama + Toeta meid + Grupid + KernelSU on, ja alati jääb, tasuta ning avatud lähtekoodiga kättesaadavaks. Sellegipoolest võid sa näidata, et hoolid, ning teha annetuse. + Mall + Vaata lähtekoodi %1$sis
Liitu meie %2$si kanaliga
+ Profiili nimi + Kohandatud + Päritud + Globaalne + Individuaalne + Võimekused + Sobimatu malli ID + SELinux kontekst + Praegune KernelSU versioon %d on liiga madal, haldur ei saa korrektselt töötada. Palun täienda versioonile %d või kõrgem! Domeen + Käivita + Sundpeata Reeglid Uuenda Mooduli allalaadimine: %s - Allalaadimise alustamine: %s Uus versioon %s on saadaval, klõpsa täiendamiseks. - Käivita - Sundpeata Taaskäivita - SELinux reeglite uuendamine ebaõnnestus: %s Muudatuste logi - Rakenduseprofiili mall - Halda kohalikke ja võrgusolevaid rakenduseprofiili malle - Loo mall - Muuda malli - ID - Sobimatu malli ID Nimi Kirjeldus + Edukalt imporditud Salvesta + Lõikelaud on tühi! Kustuta Vaata malli - Vaid lugemiseks - Malli ID juba eksisteerib! Impordi/ekspordi Impordi lõikelaualt - Ekspordi lõikelauale - Ei saa eksportida, kohalikku malli ei leitud! - Edukalt imporditud - Sünkrooni võrgumallid Malli salvestamine ebaõnnestus - Lõikelaud on tühi! - Muudatuste logi hankimine ebaõnnestus: %s + Loo mall + Halda kohalikke ja võrgusolevaid rakenduseprofiili malle + Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. + Ei saa eksportida, kohalikku malli ei leitud! + Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. + Saab kasutada WebUI silumiseks, palun luba ainult vajadusel. + Juurkasutaja andmine ebaõnnestus! Kontrolli uuendusi Rakenduse avamisel kontrolli automaatselt uuendusi - Juurkasutaja andmine ebaõnnestus! - Action - Close + Ava Luba WebView silumine - Saab kasutada WebUI silumiseks, palun luba ainult vajadusel. - Otsene paigaldus (soovitatud) - Vali fail - Paigalda ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) - Sinu seade **SUNNITAKSE** pärast taaskäivitust ebaaktiivsesse lahtrisse käivituma!\nKasuta seda valikut vaid siis, kui tegid üle-õhu uuenduse.\nJätkad? - Edasi - %1$s partitsioonitõmmis on soovitatud + Salvesta Logid Vali KMI + %1$s partitsioonitõmmis on soovitatud + Edasi + Sinu seade **SUNNITAKSE** pärast taaskäivitust ebaaktiivsesse lahtrisse käivituma!\nKasuta seda valikut vaid siis, kui tegid üle-õhu uuenduse.\nJätkad? Eemalda - Eemalda ajutiselt - Eemalda püsivalt - Taasta vaikimisi tõmmis Eemalda KernelSU ajutiselt, taasta pärast taaskäivitust algseisu. KernelSU eemaldamine (juurkasutaja ja kõik moodulid) täielikult ja püsivalt. Taasta tehase-vaiketõmmis (kui varundus eksisteerib), tavaliselt kasutatakse enne üle-õhu uuendust; kui soovid KernelSU-d eemaldada, palun kasuta \"Eemalda püsivalt\". @@ -122,241 +121,10 @@ Välgutamine õnnestus Välgutamine ebaõnnestus Valitud LKM: %s - Salvesta Logid - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Otsene paigaldus (soovitatud) + Vali fail + Paigalda ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) + Eemalda ajutiselt + Eemalda püsivalt + Taasta vaikimisi tõmmis
diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml index e142416b..874c197d 100644 --- a/manager/app/src/main/res/values-fa/strings.xml +++ b/manager/app/src/main/res/values-fa/strings.xml @@ -4,12 +4,14 @@ نصب نشده است برای نصب ضربه بزنید به درستی کار می‌کند - نسخه: %s + نسخه: %d + برنامه های با دسترسی روت: %d + ماژول‌ها: %d پشتیبانی نشده کرنل اس یو فقط هسته های gki را پشتیبانی میکند هسته - SuSFS Version نسخه برنامه + اثرانگشت وضعیت SELinux غیرفعال قانونمند @@ -20,8 +22,6 @@ غیرفعال کردن ماژول ناموفق بود: %s هیچ ماژولی نصب نشده است ماژول - Sort (Action first) - Sort (Enabled first) لغو نصب نصب نصب @@ -38,6 +38,7 @@ پاک کردن ناموفق بود: %s نسخه سازنده + overlayfs موجود نیست. مازول کار نمیکند!! تازه‌سازی نمایش برنامه های سیستمی مخفی کردن برنامه های سیستمی @@ -50,313 +51,18 @@ یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید از ما حمایت کنید KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است. - Join our %2$s channel]]> + +Join our %2$s channel ]]> + + پروفایل برنامه پیش‌فرض قالب شخصی سازی شده اسم پروفایل - Groups - Capabilities - SELinux context + Mount namespace + اثر گرفته + گلوبال + تکی جداکردن ماژول ها - Failed to update App Profile for %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Umount modules by default - The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. - Enabling this option will allow KernelSU to restore any modified files by the modules for this app. - Domain - Rules - Update - Downloading module: %s - Start downloading: %s - New version %s is available, click to upgrade. - Launch - Force stop - Restart - Failed to update SELinux rules for %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s ذخیره گزارش‌ها - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml index afcc0b2a..e8895d13 100644 --- a/manager/app/src/main/res/values-fil/strings.xml +++ b/manager/app/src/main/res/values-fil/strings.xml @@ -1,362 +1,139 @@ - Home - Hindi naka-install - Pindutin para mag-install - Gumagana - Bersyon: %s - Hindi Suportado - Sinusuportahan lang ng KernelSU ang mga kernel ng GKI ngayon - Kernel version - SuSFS Version - Bersyon ng Manager Katayuan ng SELinux - Hindi pinagana + Naka-disable Enforcing Permissive + Hindi naka-install + Panimula + I-click para i-install + Gumagana + Bersyon: %d Hindi matukoy - Superuser - Nabigong paganahin ang modyul: %s - Nabigong i-disable ang modyul: %s + Mga Modyul: %d + Hindi Suportado + Sinusuportahan lamang ng KernelSU ang mga GKI na kernel + Nabigong paganahin ang module: %s + Nabigong i-disable ang module: %s Walang naka-install na modyul Modyul - Sort (Action first) - Sort (Enabled first) - I-uninstall I-install I-install I-reboot - Mga setting - I-soft Reboot - I-reboot sa Recovery - I-reboot sa Bootloader + I-soft reboot I-reboot sa Download I-reboot sa EDL Tungkol - Sigurado ka bang gusto mong i-uninstall ang modyul %s\? + Sigurado ka bang gusto mong i-uninstall ang module na %s? Na-uninstall ang %s Nabigong i-uninstall: %s - Bersyon May-akda + Hindi available ang mga module dahil na-disable ng kernel ang OverlayFS! I-refresh Ipakita ang mga application ng system - Itago ang mga application ng system - Magpadala ng Log - Safe mode + Ipadala ang mga log I-reboot para umepekto - Hindi pinagana ang mga modyul dahil salungat ito sa Magisk! + Hindi magagamit ang mga module dahil sa isang salungatan sa Magisk! Alamin ang KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Matutunan kung paano mag-install ng KernelSU at gumamit ng mga modyul + Matuto kung paano i-install ang KernelSU at gumamit ng mga module Suportahan Kami Ang KernelSU ay, at palaging magiging, libre, at open source. Gayunpaman, maaari mong ipakita sa amin na nagmamalasakit ka sa pamamagitan ng pagbibigay ng donasyon. - Join our %2$s channel]]> - Default - Template - Custom - Pangalan ng profile + Sumali sa aming %2$s channel]]> + I-mount ang namespace + Indibidwal Mga Grupo Mga Kakayanan Konteksto ng SELinux - I-unmount ang mga modyul + I-unmount ang mga module Nabigong i-update ang App Profile para sa %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Umount modules by default - Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile. - Ang pagpapagana sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga modyul para sa aplikasyon na ito. - Domain + Ang kasalukuyang bersyon ng KernelSU %d ay masyadong mababa para gumana nang maayos ang manager. Mangyaring mag-upgrade sa bersyon %d o mas mataas! + Ang pag-enable sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga module para sa app na ito. Mga Tuntunin - Update Nagda-download ng modyul: %s Simulan ang pag-download: %s - Bagong bersyon: Available ang %s, i-click upang i-download + Bagong bersyon: Available ang %s, i-click para mag-upgrade. Ilunsad - Pilit na I-hinto + Sapilitang itigil I-restart Nabigong i-update ang mga panuntunan ng SELinux para sa: %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s + Bersyon ng manager + Mga setting + I-reboot sa Recovery + I-reboot sa Bootloader + Bersyon + I-uninstall + Itago ang mga application ng system + Pangalan ng profile + Minana + Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile. I-save ang mga Log - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Mga Superuser: %d + Bersyon ng kernel + Fingerprint + Superuser + Ii-install ang mga sumusunod na module: %1$s + Isaayos (Aksyon muna) + Isaayos (Pinagana muna) + Kumpirmahin + Safe mode + https://kernelsu.org/guide/what-is-kernelsu.html + Default + Template + Pasadya + Global + I-unmount ang mga module bilang default + Domain + I-update + Hindi mabigay ang Superuser access sa %s + Mga pagbabago + Template ng App Profile + Ipamahala ang lokal at online na template ng App Profile + Gumawa ng template + I-edit ang template + ID + Hindi wastong template ID + Pangalan + Paksa + I-save + Burahin + Tignan ang template + Read only + Umiiral na ang Template ID! + I-import/I-export + Mag-import mula sa clipboard + I-export sa clipboard + Hindi makahanap ng lokal na template na ie-export! + Matagumpay na na-import + I-sync ang mga online template + Nabigong i-save ang template + Walang laman ang clipboard! + Nabigong kunin ang mga pagbabago: %s + Tumingin para sa mga update + Awtomatikong tumingin para sa mga update kapag binubuksan ang app + Nabigong ibigay ang root! + Aksyon + Buksan + I-enable ang pag-debug ng WebView + Maaaring gamitin para i-debug ang WebUI. Mangyaring paganahin kung kinakailangan lang. + Direktang pag-install (Inirerekomenda) + Pumili ng file + I-install sa hindi aktibong slot (Pagkatapos ng OTA) + Ang iyong device ay **PIPILITIN** na i-boot sa kasalukuyang hindi aktibong slot pagkatapos ng reboot!\nGamitin lamang ang opsyon na ito kung tapos na ang OTA.\nMagpatuloy? + Susunod + Inirerekomenda ang %1$s partition image + Pumili ng KMI + I-uninstall + Pansamantalang i-uninstall + Permanenteng i-uninstall + Ibalik ang stock image + Pansamantalang i-uninstall ang KernelSU, ibabalik sa orihinal na kalagayan pagkatapos ng susunod na reboot. + Ina-uninstall ang KernelSU (root at lahat ng mga module) nang tuluyan at permanente. + Ibalik ang stock factory image (kung may umiiral na backup), kadalasan na ginagamit bago ng OTA; kung kailangan mong i-uninstall ang KernelSU, mangyaring gamitin ang \"Permanenteng i-uninstall\". + Nagfa-flash + Matagumpay ang pag-flash + Nabigo ang pag-flash + Piniling LKM: %s + Nai-save ang mga log + I-disable ang su compatibility + Pansamantalang i-disable ang kakayahan ng anumang app na makakuha ng pribilehiyong root sa pamamagitan ng su command (hindi maaapektuhan ang mga umiiral na root process). diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 6d465159..e045ebd9 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -1,109 +1,113 @@ - Accueil Non installé - Appuyez ici pour installer Fonctionnel - Version : %s - Non pris en charge + Version : %d + Super-utilisateurs : %d + Modules : %d KernelSU ne prend désormais en charge que les noyaux GKI - Noyau - Version SuSFS - Version du gestionnaire + Version du noyau + Empreinte digitale Mode SELinux Désactivé - Enforcing Permissive Inconnu Super-utilisateur - Échec de l\'activation du module : %s - Échec de la désactivation du module : %s Aucun module installé - Modules - Trier par action - Trier par activé - Désinstaller - Installer - Installer - Redémarrer - Paramètres - Redémarrage progressif - Redémarrer en mode de récupération - Redémarrer en mode bootloader - Redémarrer en mode de téléchargement - Redémarrer en mode EDL - À propos - Êtes-vous sûr(e) de vouloir désinstaller le module %s \? - %s a été désinstallé + Accueil + Appuyez ici pour installer + Non pris en charge Échec de la désinstallation : %s Version + Version du gestionnaire + Enforcing + Échec de l\'activation du module : %s + Modules + Désinstaller + Installer + Échec de la désactivation du module : %s + Redémarrer + Installer + Paramètres + Redémarrer en mode bootloader + Redémarrage logiciel + Redémarrer en mode de récupération + Redémarrer en mode EDL + À propos + %s a été désinstallé + Redémarrer en mode de téléchargement Auteur + Êtes-vous sûr(e) de vouloir désinstaller le module %s \? + Découvrir KernelSU + Les modules sont indisponibles car OverlayFS est désactivé par le noyau ! Rafraîchir Afficher les applications système Masquer les applications système - Envoyer les journaux Mode sans échec + Envoyer les journaux Redémarrez pour appliquer les modifications Les modules sont indisponibles en raison d\'un conflit avec Magisk ! - Découvrir KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Découvrez comment installer KernelSU et utiliser les modules Soutenez-nous - KernelSU est, et restera toujours, gratuit et open source. Vous pouvez cependant nous témoigner de votre soutien en nous faisant un don. + Découvrez comment installer KernelSU et utiliser les modules + KernelSU est, et restera toujours, gratuit et open-source. Néanmoins, vous pouvez nous témoigner de votre soutien en nous faisant un don. Rejoignez notre canal %2$s]]> - Par défaut Modèle + Par défaut Personnalisé Nom du profil + Espace de noms de montage + Hérité + Individuel + Contexte SELinux + Global Groupes Capacités - Contexte SELinux Démonter les modules Échec de la modification du profil d\'application de %s - La version actuelle de KernelSU (%s) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %s ou à une version supérieure ! + L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application. Démonter les modules par défaut Valeur globale par défaut pour l\'option \"Démonter les modules\" dans les profils d\'application. Lorsque l\'option est activée, les modifications apportées au système par les modules sont supprimées pour les applications qui n\'ont pas de profil défini. - L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application. Domaine Règles Mettre à jour Téléchargement du module : %s - Début du téléchargement de : %s - La nouvelle version %s est disponible, appuyez ici pour mettre à jour. Lancer - Forcer l\'arrêt + La nouvelle version %s est disponible, appuyez pour mettre à jour ! + Début du téléchargement de : %s + Forcer l\'arrêt Relancer l\'application - Échec de la mise à jour des règles SELinux pour : %s - Journal des modifications - Modèles de profils d\'application - Gérer les modèles de profils d\'application locaux et en ligne - Créer un modèle - Modifier le modèle - ID - ID de modèle invalide - Nom - Description - Enregistrer - Supprimer - Voir le modèle - Lecture seule - L\'ID du modèle existe déjà ! - Importer/exporter - Importer à partir du presse-papiers + Échec de la mise à jour des règles SELinux pour %s + La version actuelle de KernelSU (%d) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %d ou à une version supérieure ! + Importation réussie Exporter vers le presse-papiers Impossible de trouver un modèle local à exporter ! - Importation réussie - Synchroniser les modèles en ligne - Échec de l\'enregistrement du modèle - Le presse-papiers est vide ! + L\'ID du modèle existe déjà ! + Journal des modifications + Importer à partir du presse-papiers Échec de récupération du journal des modifications : %s - Vérifier les mises à jour - Vérifier automatiquement les mises à jour à l\'ouverture de l\'application - Échec de l\'octroi des privilèges root ! - Action - Fermer - Activer le débogage WebView + Nom + ID de modèle invalide + Synchroniser les modèles en ligne + Créer un modèle + Lecture seule + Importer/exporter + Échec de l\'enregistrement du modèle + Modifier le modèle + ID + Modèles de profils d\'application + Description + Enregistrer + Gérez les modèles de profils d\'application locaux et en ligne. + Supprimer + Le presse-papiers est vide ! + Voir le modèle + Vérifier automatiquement les mises à jour à l\'ouverture de l\'application. + Rechercher des mises à jour + Débogage WebView Peut être utilisé pour déboguer WebUI. Activez uniquement cette option si nécessaire. + Échec de l\'octroi des privilèges root ! + Ouvrir Installation directe (recommandé) Sélectionner un fichier Installer dans l\'emplacement inactif (après OTA) @@ -117,248 +121,36 @@ Désinstaller temporairement Désinstaller définitivement Restaurer l\'image d\'origine - Désinstaller KernelSU temporairement et rétablir l\'état original au redémarrage suivant. - Désinstallation complète et permanente de KernelSU (root et tous les modules). Restaurer l\'image d\'origine d\'usine (s\'il en existe une sauvegarde). Utilisé généralement avant une mise à jour OTA ; si vous devez désinstaller KernelSU, utilisez plutôt l\'option \"Désinstaller définitivement\". Flash en cours Flash réussi Échec du flash LKM sélectionné : %s + Désinstallation complète et permanente de KernelSU (root et tous les modules). + Désinstaller temporairement KernelSU et rétablir l\'état original au redémarrage suivant. Enregistrer les journaux + Trier par action + Trier par activé + Action Journaux enregistrés - - confirmer l\'installation du module %1$s? - module inconnu - - Confirmer la restauration - Cette opération va écraser les modules existants. Continuer ? + Désactiver la compatibilité avec su + Désactivez la possibilité de toute application d\'obtenir les privilèges root via la commande su (les processus root existants ne seront pas affectés). + Les modules suivants vont être installés : %1$s + Impossible d\'octroyer les autorisations superutilisateur à %s Confirmer - Annuler - - Sauvegarde réussie (tar.gz) - Échec de la sauvegarde : %1$s - modules de sauvegarde - Restaurer les modules - - Succès de la sauvegarde, redémarrer - Échec de la restauration : %1$s - Redémarrer - Erreur inconnue - - L\'exécution de la commande a échoué : %1$s - - Sauvegarde de la liste blanche réussie - La sauvegarde de la liste d\'autorisations a échoué : %1$s - Confirmer la restauration de la liste blanche - Cette opération écrasera la liste blanche actuelle. Continuer ? - Liste blanche restaurée avec succès - La restauration de la liste d\'autorisations a échoué : %1$s - Sauvegarder la liste blanche - Restaurer la liste blanche - Arrière-plan personnalisé de l\'application - Image as arrière-plan - Transparence de la barre de navigation - Version Android - Modèle du téléphone - Donner un super-utilisateur à %s n\'est pas autorisé - Désactiver la compatibilité su - Désactiver temporairement l\'accès des applications aux privilèges root via la commande su (les processus root existants ne seront pas affectés). - Êtes-vous sûr de vouloir installer les modules %1$d suivants ? \n\n%2$s - Autres configurations - SELinux - Activé - Désactivé - Me simple - Masque les cartes inutiles lorsqu\'il est activé - Masquer la version du noyau - Masquer la version du noyau - Masquer les autres infos - Masque des informations sur le nombre de super utilisateurs, de modules et de modules KPM sur la page d\'accueil - Masquer le statut SuSFS - Masquer les informations de la carte de lien sur la page d\'accueil - Masquer le statut du lien de la carte - Masquer les informations de la carte de lien sur la page d\'accueil - Thème - Suivre le système - Clair - Sombre - Crochet manuel - Couleur dynamique - Couleurs dynamiques en utilisant des thèmes système - Choisir une couleur de thème - Bleu - Vert - Violet - Orange - Rose - Gris - Jaune - Install Anykernel3 - Fichier noyau AnyKernel3 - Nécessite les privilèges root - Traitement terminé - Redémarrer immédiatement ? - Oui - Non - Échec du redémarrage - KPM - Aucun module de noyau installé pour le moment - Version - Auteur - Désinstaller - Désinstallé avec succès - Échec de la désinstallation : - Chargement du module kpm réussi - Le chargement du module kpm a échoué - Paramètres - Exécuter - Version de KPM - Fermer - Les fonctions suivantes du module du noyau ont été développées par KernelPatch et modifiées pour inclure les fonctions du module du noyau de SukiSU Ultra - SukiSU Ultra attend avec impatience - Succès - Echoué - SukiSU Ultra sera une branche relativement indépendante de KSU dans le futur, mais nous apprécions toujours le KernelSU officiel, MKSU etc. pour leurs contributions! - Non pris en charge - Pris en charge - Noyau non corrigé - Noyau non configuré - Paramètres personnalisés - KPM Installé - Charger - Intégrer - Veuillez sélectionner : %1\$s Mode d\'installation du module \n\nCharge : Chargez temporairement le module \nIntégré: Installez définitivement dans le système - Impossible de vérifier si le fichier du module existe - Couleur du thème - Type de fichier incorrect ! Veuillez sélectionner un fichier .kpm. - Désinstaller - Le KPM suivant sera désinstallé : %s - Utilisez deux doigts pour zoomer l\'image, et un doigt pour le faire glisser pour ajuster la position - Remise à disposition - - Flash terminé - - Préparation de… - Nettoyage des fichiers… - Copie des fichiers… - Extraction de l\'outil flash… - Mise à jour du script… - Flash du noyau… - Flash complété - - Sélectionnez l\'emplacement de Flash - Veuillez sélectionner l\'emplacement cible pour le démarrage du flash - Slot A - Slot B - LKM sélectionné : %1$s - Obtention de l\'emplacement original - Définition de l\'emplacement spécifié - Restaurer l\'emplacement par défaut - Emplacement actuel - - Copie échouée - Erreur inconnue - Échec du flash - - Réparation/installation LKM - Flash du noyau… - Version du noyau:%1$s - Utilisation de l\'outil de correctifs:%1$s - Configurer - Paramètres de l\'application - Outils - - Application introuvable - SELinux activé - SELinux désactivé - La modification du statut SELinux a échoué - Paramètres avancés - Choisir les boutons à afficher - Reviens - Fond d\'écran défini avec succès - Fond d\'écran personnalisé supprimé - Icône alternative - Changer l\'icône du lanceur en icône de KernelSU. - Icône changée - - Afficher la fonction KPM - Masque les informations et fonctions KPM dans la barre d\'accueil et en bas - - Sélectionnez le moteur WebUI à utiliser - Sélectionner automatiquement - Forcer l\'utilisation de WebUI X - Utilisation obligatoire de KSU WebUI - Injecter Eruda dans WebUI X - Injectez une console de débogage dans WebUI X pour faciliter le débogage. Nécessite que le débogage soit activé. - - DPI appliqué - Ajuster la densité d\'affichage de l\'écran pour l\'application actuelle uniquement - Petit - Moyenne - Grand - surtaille - Personnalisable - Application des paramètres DPI - Confirmer le changement de DPI - Êtes-vous sûr de vouloir changer le DPI de l\'application de %1$d à %2$d? - L\'application doit être redémarrée pour appliquer les nouveaux paramètres de DPI, n\'affecte pas la barre d\'état du système ou d\'autres applications - Le DPI a été réglé sur %1$d, effectif après le redémarrage de l\'application - - Langue de l\'application - Suivre le paramètre système - Ajustement de l\'obscurité de la carte - - code d\'erreur - Veuillez vérifier le journal - Module en cours d\'installation %1$d/%2$d - %d a échoué à installer un nouveau module - Le téléchargement du modèle a échoué - Clignotement du noyau - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - En haut - En Bas - Sélectionné - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Utiliser un fichier LKM local + Seuls les fichiers .ko sont pris en charge + Désactiver kernel umount + Désactiver le comportement contrôlé par KernelSU de la fonction umount au niveau du noyau. + Traitement… + Tirez pour actualiser + Relâchez pour actualiser + Actualisation… + Actualisé avec succès + Rechercher des mises à jour des modules + Activer la sécurité améliorée + Activer des règles de sécurité plus strictes. + Par défaut + Activer temporairement + Activer définitivement diff --git a/manager/app/src/main/res/values-hi/strings.xml b/manager/app/src/main/res/values-hi/strings.xml index cde029c2..de1c2795 100644 --- a/manager/app/src/main/res/values-hi/strings.xml +++ b/manager/app/src/main/res/values-hi/strings.xml @@ -1,362 +1,84 @@ - होम - इंस्टाल नहीं हुआ - इंस्टाल करने के लिए क्लिक करें - काम कर रहा है - वर्जन: %s - सपोर्ट नहीं करता है - KernelSU अभी केवल GKI कर्नल्स को सपोर्ट करता है - कर्नल - SuSFS Version - मैनेजर वर्जन - SELinux स्थिति - डिसेबल्ड (बंद) - एनफोर्सिंग - पर्मिसिव + प्रभाव में होने के लिए रीबूट करें + जानें कि KernelSU कैसे स्थापित करें और मॉड्यूल का उपयोग कैसे करें अज्ञात - सुपरयूजर + सिस्टम एप्प दिखाए + %s अनइंस्टॉल सफल हुआ + मॉड्यूल्स अनमाउंट करें + लॉग भेजे + डिसेबल्ड (बंद) + हमें प्रोत्साहन दें + Inherited + मॉड्यूल बंद कर दिए गए हैं क्योंकि यह मैजिक के साथ टकरा रहे है! + क्या बदलाव हुए है + पर्मिसिव + डाउनलोड में रिबूट करें + डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें + इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। + Individual %s मॉड्यूल चालू करने में विफल + जबर्दस्ती बंद करें + EDL मोड में रिबूट करें + फिर से चालू करें + क्षमताएं + सुपरयूजर : %d + %s की डाउनलोडिंग स्टार्ट करें + Global + ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। + मॉड्यूल्स : %d + एनफोर्सिंग + SELinux context + फिंगरप्रिंट + डिफॉल्ट + लॉन्च करें + सेफ मोड + मैनेजर के ठीक से काम करने के लिए वर्तमान KernelSU वर्जन %d बहुत कम है। कृपया वर्जन %d या उच्चतर में अपग्रेड करें! + रिकवरी में रिबूट करें + सॉफ्ट रिबूट + प्रोफाइल का नाम + KernelSU मुफ़्त और ओपन सोर्स और हमेशा रहेगा। हालाँकि आप दान देकर हमें दिखा सकते हैं कि आप संरक्षण करते हैं। + अनइंस्टॉल करें + Namspace माउंट करें + इंस्टाल करें + इंस्टाल करने के लिए क्लिक करें + नियम + समूह + Overlayfs उपलब्ध नहीं है, मॉड्यूल काम नहीं कर सकता ! + मॉड्यूल + निर्माता + हमारे बारे में + वर्जन: %d + रीबूट करें + KernelSU अभी केवल GKI कर्नल्स को सपोर्ट करता है + SELinux स्थिति + सिस्टम एप्प छिपाए + वर्जन + सपोर्ट नहीं करता है + डोमेन + होम + कस्टम + टेम्पलेट + रिफ्रेश + %s मॉड्यूल डाउनलोड हो रहा है + अपडेट + KernelSU सीखें + क्या आप सच में मॉड्यूल %s को अनइंस्टॉल करना चाहते हैं\? + %s अनइंस्टल करने में असफल + सुपरयूजर + सेटिंग + काम कर रहा है %s मॉड्यूल बंद करने में विफल कोई मॉड्यूल इंस्टाल नहीं हुआ - मॉड्यूल - Sort (Action first) - Sort (Enabled first) - अनइंस्टॉल करें - इंस्टाल करें इंस्टाल करें - रीबूट करें - सेटिंग - सॉफ्ट रिबूट - रिकवरी में रिबूट करें - बुटलोडर में रिबूट करें - डाउनलोड में रिबूट करें - EDL मोड में रिबूट करें - हमारे बारे में - क्या आप सच में मॉड्यूल %s को अनइंस्टॉल करना चाहते हैं\? - %s अनइंस्टॉल सफल हुआ - %s अनइंस्टल करने में असफल - वर्जन - निर्माता - रिफ्रेश - सिस्टम एप्प दिखाए - सिस्टम एप्प छिपाए - लॉग भेजे - सेफ मोड - प्रभाव में होने के लिए रीबूट करें - मॉड्यूल बंद कर दिए गए हैं क्योंकि यह मैजिक के साथ टकरा रहे है! - KernelSU सीखें - https://kernelsu.org/guide/what-is-kernelsu.html - जानें कि KernelSU कैसे स्थापित करें और मॉड्यूल का उपयोग कैसे करें - हमें प्रोत्साहन दें - KernelSU मुफ़्त और ओपन सोर्स और हमेशा रहेगा। हालाँकि आप दान देकर हमें दिखा सकते हैं कि आप संरक्षण करते हैं। - Join our %2$s channel]]> - डिफॉल्ट - टेम्पलेट - कस्टम - प्रोफाइल का नाम - समूह - क्षमताएं - SELinux context - मॉड्यूल्स अनमाउंट करें + कर्नल + इंस्टाल नहीं हुआ %s के लिए ऐप प्रोफ़ाइल अपडेट करने में विफल - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें - ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। - इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। - डोमेन - नियम - अपडेट - %s मॉड्यूल डाउनलोड हो रहा है - %s की डाउनलोडिंग स्टार्ट करें - नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें - लॉन्च करें - जबर्दस्ती बंद करें - फिर से चालू करें + https://kernelsu.org/guide/what-is-kernelsu.html %s के लिए SELinux नियमों को अपटेड करने में विफल - क्या बदलाव हुए है - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s + बुटलोडर में रिबूट करें + %1$s पर स्रोत कोड देखें
हमारे %2$s चैनल से जुड़ें
+ मैनेजर वर्जन + नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें लॉग सहेजें - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml index ab688415..594ae453 100644 --- a/manager/app/src/main/res/values-hr/strings.xml +++ b/manager/app/src/main/res/values-hr/strings.xml @@ -1,33 +1,39 @@ + Prikažite sistemske aplikacije + Sakrijte sistemske aplikacije + Pošaljite izvještaje + Sigurnosni mod + Ponovno pokrenite da bi proradilo + Nije uspjelo ažuriranje SELinux pravila za %s Početna Nije instalirano + Verzija: %d Kliknite da instalirate Radi - Verzija: %s + Superkorisnici: %d + Moduli: %d Nepodržano - KernelSU samo podržava GKI kernele sad - Kernel - SuSFS Version - Verzija Voditelja - SELinux stanje + KernelSU sada samo podržava GKI kernele + Verzija kernela + Verzija voditelja + Otisak prsta Isključeno U Provođenju Permisivno + SELinux stanje Nepoznato Superkorisnik Neuspješno uključivanje module: %s Neuspješno isključivanje module: %s Nema instaliranih modula Modula - Sort (Action first) - Sort (Enabled first) Deinstalirajte Instalirajte Instalirajte Ponovno pokrenite Postavke - Lagano Ponovno pokretanje + Lagano ponovno pokretanje Ponovno pokrenite u Oporavu Ponovno pokrenite u Pogonski Učitavalac Ponovno pokrenite u Preuzimanje @@ -39,324 +45,104 @@ Verzija Autor Osvježi - Prikažite sistemske aplikacije - Sakrijte sistemske aplikacije - Pošaljite Izvještaj - Sigurnosni mod - Ponovno pokrenite da bi proradilo - Module su isključene jer je u sukobu sa Magisk-om! + Moduli nisu dostupni jer je OverlayFS onemogućen od strane kernela! + Moduli nisu dostupni zbog sukoba s Magiskom! Naučite KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Naučite kako da instalirate KernelSU i da koristite module Podržite Nas - KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. - Join our %2$s channel]]> + KernelSU je i uvijek će biti besplatan i otvorenog koda. Međutim, možete nam pokazati da vam je stalo donacijom + Pridružite se našem %2$s kanalu]]> Zadano Šablon Prilagođeno Naziv profila + Naslijeđen + Imenski prostor nosača + Ažuriranje Profila Aplikacije za %s nije uspjelo + Globalan + Pojedinačan + Umount module Grupe Sposobnosti SELinux kontekst - Umount module - Ažuriranje Profila Aplikacije za %s nije uspjelo - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! + Trenutna verzija KernelSU-a %d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %d ili noviju! Umount module po zadanom - Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. - Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. + Globalna zadana vrijednost za \"Umount module\" u profilu aplikacije. Ako je omogućeno, uklonit će sve modifikacije modula sustava za aplikacije koje nemaju postavljen profil Domena + Omogućavanje ove opcije omogućit će KernelSU-u da vrati sve izmijenjene datoteke od strane modula za ovu aplikaciju Pravila Ažuriranje Preuzimanje module: %s Započnite sa preuzimanjem: %s - Nova verzija: %s je dostupna, kliknite da preuzmete + Nova verzija %s je dostupna, kliknite za nadogradnju Pokrenite - Prisilno Zaustavite + Prisilno zaustavi Resetujte - Neuspješno ažuriranje SELinux pravila za: %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template + Spremi zapise + Sljedeći moduli će biti instalirani: %1$s + Sortiraj (Prvo radnja) + Sortiraj (prvo omogućeno) + Potvrdi + Predložak profila aplikacije + Upravljanje lokalnim i online predloškom profila aplikacije + Izradi predložak + Uredi predložak ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s - Spremi Zapise - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Nevažeći ID predloška + Naziv + Opis + Spremi + Izbriši + Prikaži predložak + Samo za čitanje + ID predloška već postoji! + Uvoz/Izvoz + Uvezi iz međuspremnika + Izvezi u međuspremnik + Nije moguće pronaći lokalni predložak za izvoz! + Uspješno uvezeno + Sinkronizacija online predložaka + Spremanje predloška nije uspjelo + Međuspremnik je prazan! + Dohvaćanje popisa promjena nije uspjelo: %s + Provjeri za ažuriranja + Automatski provjeri za ažuriranja prilikom otvaranja aplikacije + Dodjeljivanje root pristupa nije uspjelo! + Radnja + Otvori + Omogući otklanjanje pogrešaka u WebViewu + Može se koristiti za otklanjanje pogrešaka u WebUI-ju. Omogućite samo kada je potrebno + Izravna instalacija (preporučeno) + Odaberite datoteku + Instaliraj u neaktivni utor (nakon OTA) + Vaš će uređaj biti **PRISILNO** pokrenuti se u trenutno neaktivni utor nakon ponovnog pokretanja!\nKoristite ovu opciju tek nakon što je OTA završen.\nNastaviti? + Dalje + Koristi lokalnu LKM datoteku + Podržane su samo .ko datoteke + Preporučuje se particija %1$s + Odaberi KMI + Deinstaliraj + Privremeno deinstaliraj + Trajno deinstaliraj + Vrati stock image + Privremeno deinstaliraj KernelSU, vrati u izvorno stanje nakon sljedećeg ponovnog pokretanja + Potpuno i trajno deinstaliranje KernelSU-a (Root i svi moduli) + Vrati stock factory image (ako postoji sigurnosna kopija), obično se koristi prije OTA-e; ako trebate deinstalirati KernelSU, koristite \"Trajno deinstaliraj\" + Flešanje + Uspješno flešano + Neuspješno flešanje + Odabrani LKM: %s + Zapisi spremljeni + Onemogući su kompatibilnost + Privremeno onemogući bilo kojoj aplikaciji dobivanje root privilegija putem naredbe ⁠su (postojeći root procesi neće biti pogođeni). + Onemogući kernel umount + Privremeno onemogući ponašanje kernel-level unmount koje kontrolira KernelSU. + Obrada… + Povuci dolje za osvježiti + Otpusti za osvježiti + Osvježavanje… + Uspješno osvježeno + Nije moguće odobriti Superuser pristup za %s + Zapis promjena diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index ba3813a0..5604c989 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -1,17 +1,56 @@ + Működik + Verzió: %d + Modulok: %d + A KernelSU jelenleg csak GKI kerneleket támogat + Kernel + Alkalmazás verziója + Ujjlenyomat + Letiltva + Újraindítás letöltő módba + Újraindítás EDL-be + Névjegy + Biztos benne hogy eltávolítja a következő modult: %s? + Nem sikerült eltávolítani: %s + Készítő + A modulok nem érhetők el, mivel az OverlayFS-t a kernel letiltotta. + Frissítés + Rendszeralkalmazások megjelenítése + Rendszeralkalmazások elrejtése + Biztonságos mód + A modulok nem érhetők el a Magiskkel való ütközés miatt! + Tudjon meg többet a KernelSU-ról + Ismerje meg a KernelSU telepítését és a modulok használatát + Támogasson minket + Tekintse meg a forráskódot a %1$s-on
Csatlakozzon a %2$s csatornánkhoz
+ Alapértelmezett + Sablon + Egyedi + Profil neve + Névtér csatlakoztatása + Örökölt + https://kernelsu.org/guide/what-is-kernelsu.html + Különálló + Csoportok + Jogosultságok + SELinux kontextus + Modulok leválasztása alapértelmezetten + Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. + Tartomány + Szabályok + Frissítés + Modul letöltése: %s + Letöltés indítása: %s + Indítás + Kényszerített leállítás + újraindítás Kezdőlap Nincs telepítve Kattintson a telepítéshez - Működik - Verzió: %s + Engedélyezett alkalmazások: %d Nem támogatott - A KernelSU jelenleg csak GKI kerneleket támogat - Kernel - SuSFS Version - Alkalmazás verziója SELinux állapot - Letiltva Kényszerített Engedélyezett Ismeretlen @@ -20,8 +59,6 @@ Nem sikerült letiltani a következő modult: %s Nincs telepített modul Modulok - Sort (Action first) - Sort (Enabled first) Eltávolítás Telepítés Telepítés @@ -30,333 +67,66 @@ Rendszerfelület újraindítása Újraindítás recovery-módba Újraindítás bootloader-módba - Újraindítás letöltő módba - Újraindítás EDL-be - Névjegy - Biztos benne hogy eltávolítja a következő modult: %s? %s eltávolítva - Nem sikerült eltávolítani: %s Verzió - Készítő - Frissítés - Rendszeralkalmazások megjelenítése - Rendszeralkalmazások elrejtése Naplók küldése - Biztonságos mód Indítsa újra a készüléket a változások érvényesítéséhez - A modulok nem érhetők el a Magiskkel való ütközés miatt! - Tudjon meg többet a KernelSU-ról - https://kernelsu.org/guide/what-is-kernelsu.html - Ismerje meg a KernelSU telepítését és a modulok használatát - Támogasson minket A KernelSU ingyenes, nyílt forráskódú és mindig is az lesz. Ön azonban adományozással megmutathatja, hogy törődik a projekttel. - Join our %2$s channel]]> - Alapértelmezett - Sablon - Egyedi - Profil neve - Csoportok - Jogosultságok - SELinux kontextus + Globális Modulok leválasztása Nem sikerült frissíteni az App Profilt ehhez: %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Modulok leválasztása alapértelmezetten A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva. - Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. - Tartomány - Szabályok - Frissítés - Modul letöltése: %s - Letöltés indítása: %s Elérhető az új, %s verzió, kattintson a frissítéshez. - Indítás - Kényszerített leállítás - újraindítás Nem sikerült frissíteni az SELinux szabályokat a következőhöz: %s - Változások - App Profile sablon - Az App Profile helyi és online sablonjának kezelése - Sablon készítése - Sablon szerkesztése - ID - Hibás sablon ID - Név - Leírás - Mentés - Törlés - Sablon megtekintése - Csak olvasható - A sablon ID már létezik! - Import/Export - Importálás a vágólapról + A jelenlegi KernelSU verzió %d túlságosan elavult a megfelelő működéshez. Kérjük frissítsen a %d verzióra vagy újabbra! + Sikeresen importálva Exportálás a vágólapról Nem található helyi sablon az exportáláshoz! - Sikeresen importálva - Online sablonok szinkronizálása - A sablon mentése sikertelen - A vágólap üres! + A sablon ID már létezik! + Változások + Importálás a vágólapról A változásnapló lekérése nem sikerült: %s - Frissítés ellenőrzése - Automatikusan keressen frissítéseket az alkalmazás megnyitásakor - A root jog megadása sikertelen! - Művelet - Close - WebView hibakeresés engedélyezése + Név + Hibás sablon ID + Online sablonok szinkronizálása + Sablon készítése + Csak olvasható + Import/Export + A sablon mentése sikertelen + Sablon szerkesztése + ID + App Profile sablon + Leírás + Mentés + Az App Profile helyi és online sablonjának kezelése + Törlés + A vágólap üres! + Sablon megtekintése + Naplók mentése A WebUI hibakeresésére használható, csak szükség esetén engedélyezze. - Közvetlen telepítés (Ajánlott) - Fájl kiválasztása - Telepítés inaktív helyre (OTA után) - Az eszköze **KÉNYSZERÍTETTEN** a jelenleg inaktív helyről fog indulni újraindítás után!\nCsak az OTA befejezése után használja.\nFolytatja? - Következő + WebView hibakeresés engedélyezése + Megnyitás + Végleges eltávolítás %1$s partíció képfájl ajánlott KMI kiválasztása - Eltávolítás + Következő Ideiglenes eltávolítás - Végleges eltávolítás - Eredeti képfájl visszaállítása A KernelSU ideiglenes eltávolítása, az eredeti állapot visszaállítása a következő újraindítás után. - A KernelSU eltávolítása (root és az összes modul) teljesen és véglegesen. - Állítsa vissza a gyári képfájlt (ha létezik biztonsági mentés). Általában OTA előtt használják. Ha a KernelSU-t szeretné eltávolítani, használja a végleges eltávolítás opciót. + Eltávolítás Telepítés Sikeres telepítés - Sikertelen telepítés Kiválasztott LKM: %s - Naplók mentése + Sikertelen telepítés + A root jog megadása sikertelen! + Telepítés inaktív helyre (OTA után) + Fájl kiválasztása + A KernelSU eltávolítása (root és az összes modul) teljesen és véglegesen. + Eredeti képfájl visszaállítása + Művelet + Közvetlen telepítés (Ajánlott) + Az eszköze **KÉNYSZERÍTETTEN** a jelenleg inaktív helyről fog indulni újraindítás után!\nCsak az OTA befejezése után használja.\nFolytatja? + Állítsa vissza a gyári képfájlt (ha létezik biztonsági mentés). Általában OTA előtt használják. Ha a KernelSU-t szeretné eltávolítani, használja a végleges eltávolítás opciót. + Frissítés ellenőrzése + Automatikusan keressen frissítéseket az alkalmazás megnyitásakor Mentett naplók - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-idn/strings.xml b/manager/app/src/main/res/values-idn/strings.xml deleted file mode 100644 index 3dbe03ec..00000000 --- a/manager/app/src/main/res/values-idn/strings.xml +++ /dev/null @@ -1,537 +0,0 @@ - - - Beranda - Tidak Terpasang - Klik untuk Memasang - Berfungsi - Versi: %s - Tidak Didukung - Driver KernelSU tidak terdeteksi di kernel Anda. Mungkin Anda menggunakan kernel yang salah. - Versi Kernel - Versi SuSFS - Versi Manajer - Status SELinux - Dinonaktifkan - Ditegakkan - Permisi - Tidak Diketahui - Superuser - Gagal mengaktifkan modul: %s - Gagal menonaktifkan modul: %s - Tidak ada modul terpasang - Modul - Urutkan (Aksi Terlebih Dahulu) - Urutkan (Aktif Terlebih Dahulu) - Copot Pemasangan - Pasang - Pasang - Muat Ulang - Pengaturan - Muat Ulang Lunak - Muat Ulang ke Recovery - Muat Ulang ke Bootloader - Muat Ulang ke Mode Download - Muat Ulang ke Mode EDL - Tentang - Apakah Anda yakin ingin mencopot pemasangan modul %s? - %s telah dicopot - Gagal mencopot pemasangan: %s - Versi - Penulis - Segarkan - Tampilkan Aplikasi Sistem - Sembunyikan Aplikasi Sistem - Kirim Log - Mode Aman - Muat ulang untuk menerapkan - Modul tidak tersedia karena konflik dengan Magisk! - Pelajari tentang KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Pelajari cara memasang KernelSU dan menggunakan modul - Dukung Kami - KernelSU bersifat gratis dan open source, sekarang dan selamanya. Namun, Anda dapat menunjukkan dukungan Anda dengan melakukan donasi. - Gabung ke saluran %2$s kami]]> - Profil Aplikasi - Bawaan - Templat - Khusus - Nama Profil - Grup - Kemampuan - Konteks SELinux - Lepas Kait Modul - Gagal memperbarui profil aplikasi untuk %s - Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manajer dengan benar. Harap perbarui ke versi %s atau yang lebih tinggi! - Lepas kait modul secara bawaan - Nilai bawaan global untuk \"Lepas Kait Modul\" dalam profil aplikasi. Jika diaktifkan, ini akan menghapus semua perubahan sistem yang dibuat oleh modul untuk aplikasi tanpa profil yang ditetapkan. - Mengaktifkan opsi ini akan memungkinkan KernelSU untuk memulihkan file yang diubah oleh modul untuk aplikasi ini. - Domain - Aturan - Perbarui - Mengunduh modul: %s - Memulai pengunduhan: %s - Versi baru %s tersedia, klik untuk memperbarui. - Jalankan - Paksa Hentikan - Jalankan Ulang - Gagal memperbarui aturan SELinux untuk %s - Catatan Perubahan - Templat Profil Aplikasi - Kelola templat profil aplikasi lokal dan daring - Buat Templat - Edit Templat - ID - ID Templat Tidak Valid - Nama - Deskripsi - Simpan - Hapus - Lihat Templat - Hanya Baca - ID Templat sudah ada! - Impor/Ekspor - Impor dari Papan Klip - Ekspor ke Papan Klip - Tidak ditemukan templat lokal untuk diekspor! - Berhasil diimpor - Sinkronkan Templat Daring - Gagal menyimpan templat - Papan klip kosong! - Gagal memuat catatan perubahan: %s - Periksa Pembaruan - Secara otomatis memeriksa pembaruan saat membuka aplikasi - Gagal memberikan hak akses root! - Aksi - Tutup - Aktifkan Debug WebView - Dapat digunakan untuk mendebug WebUI. Harap aktifkan hanya jika diperlukan. - Pemasangan Langsung (Disarankan) - Pilih Gambar untuk Dipatch - Pasang ke Slot Tidak Aktif (Setelah OTA) - Perangkat Anda akan **DIPAKSA** untuk boot ke slot tidak aktif saat ini setelah reboot! -Gunakan opsi ini hanya setelah OTA selesai. -Lanjutkan? - Lanjut - Disarankan gambar partisi %1$s - Pilih KMI - Copot Pemasangan - Copot Pemasangan Sementara - Copot Pemasangan Permanen - Pulihkan Gambar Bawaan - Copot pemasangan KernelSU secara sementara, kembalikan ke keadaan awal setelah reboot berikutnya. - Copot pemasangan KernelSU secara lengkap dan permanen (Root dan semua modul). - Pulihkan gambar bawaan pabrik (jika cadangan tersedia), biasanya digunakan sebelum OTA; jika ingin mencopot KernelSU, gunakan \"Copot Pemasangan Permanen\". - Mem-flash - Flash Berhasil - Flash Gagal - LKM Terpilih: %s - Simpan Log - Log Disimpan - - Konfirmasi pemasangan modul %1$s? - modul tidak dikenal - - Konfirmasi Pemulihan Modul - Operasi ini akan menimpa semua modul yang ada. Lanjutkan? - Konfirmasi - Batal - - Pencadangan Berhasil (tar.gz) - Gagal membuat cadangan: %1$s - cadangan modul - pulihkan modul - - Modul berhasil dipulihkan, perlu reboot - Gagal memulihkan: %1$s - Muat Ulang Sekarang - Kesalahan Tidak Diketahui - - Gagal mengeksekusi perintah: %1$s - - Pencadangan daftar izin berhasil - Gagal membuat cadangan daftar izin: %1$s - Konfirmasi Pemulihan Daftar Izin - Operasi ini akan menimpa daftar izin saat ini. Lanjutkan? - Daftar izin berhasil dipulihkan - Gagal memulihkan daftar izin: %1$s - Cadangkan Daftar Izin - Pulihkan Daftar Izin - Latar Belakang Aplikasi Khusus - Pilih gambar sebagai latar belakang - Transparansi Panel Navigasi - Versi Android - Model Perangkat - Pemberian hak superuser untuk %s tidak diizinkan - Nonaktifkan Kompatibilitas su - Sementara mencegah aplikasi mana pun mendapatkan hak root melalui perintah su (proses root yang ada tidak akan terpengaruh). - Apakah Anda yakin ingin memasang %1$d modul berikut? -%2$s - Pengaturan Lainnya - SELinux - Diaktifkan - Dinonaktifkan - Mode Sederhana - Menyembunyikan kartu yang tidak perlu saat diaktifkan - Sembunyikan Versi Kernel - Menyembunyikan versi kernel - Sembunyikan Informasi Lainnya - Menyembunyikan titik merah yang menunjukkan jumlah superuser, modul, dan modul KPM di halaman navigasi bawah - Sembunyikan Status SuSFS - Menyembunyikan informasi status SuSFS di halaman beranda - Sembunyikan Kartu Tautan - Menyembunyikan informasi di kartu tautan di halaman beranda - Sembunyikan Baris Tag Modul - Menyembunyikan label nama folder dan ukuran di kartu modul - Tema - Ikuti Sistem - Terang - Gelap - Hook Manual - Warna Dinamis - Warna dinamis menggunakan tema sistem - Pilih Warna Tema - Biru - Hijau - Ungu - Oranye - Merah Muda - Abu-abu - Kuning - Pasang Anykernel3 - Flash file kernel AnyKernel3 - Diperlukan hak akses root - Pembersihan Selesai - Muat ulang sekarang? - Ya - Tidak - Gagal memuat ulang - KPM - Saat ini tidak ada modul kernel yang terpasang - Versi - Penulis - Copot Pemasangan - Berhasil dicopot - Gagal mencopot - Berhasil memuat modul kpm - Gagal memuat modul kpm - Parameter - Jalankan - Versi KPM - Tutup - Fitur modul kernel berikut dikembangkan oleh KernelPatch dan dimodifikasi untuk menyertakan fitur modul kernel SukiSU Ultra - SukiSU Ultra menantikan - Berhasil - Gagal - Ke depannya, SukiSU Ultra akan menjadi cabang KSU yang relatif independen, tetapi kami tetap berterima kasih kepada KernelSU resmi, MKSU, dan lainnya atas kontribusi mereka! - Tidak Didukung - Didukung - Kernel Belum Di-patch - Kernel Belum Diaktifkan - Pengaturan Khusus - Pemasangan KPM - Muat - Tanamkan - Silakan pilih: %1$s mode pemasangan modul -Muat: Secara sementara memuat modul -Tanamkan: Secara permanen memasang ke sistem - Gagal memeriksa keberadaan file modul - Warna Tema - Jenis file tidak valid! Harap pilih file .kpm. - Copot Pemasangan - Akan mencopot KPM berikut: %s - Gunakan dua jari untuk memperbesar gambar dan satu jari untuk menyeret, untuk menyesuaikan posisi - Provisi Ulang - - Flash Selesai - - Menyiapkan… - Membersihkan file… - Menyalin file… - Mengekstrak alat flash… - Menambal skrip flash… - Mem-flash kernel… - Flash Selesai - - Pilih Slot untuk Flash - Silakan pilih slot target untuk flashing boot - Slot A - Slot B - Slot Terpilih: %1$s - Mendapatkan slot asli - Mengatur slot target - Mengembalikan slot bawaan - Slot sistem bawaan saat ini: %1$s - - Gagal menyalin - Kesalahan Tidak Diketahui - Flash Gagal - - Pemulihan/Pemasangan LKM - Flash AnyKernel3 - Versi Kernel: %1$s - Alat patch yang digunakan: %1$s - Konfigurasi - Pengaturan Aplikasi - Alat - - Aplikasi tidak ditemukan - SELinux diaktifkan - SELinux dinonaktifkan - Gagal mengubah status SELinux - Pengaturan Lanjutan - Sesuaikan Bilah Alat - Kembali - Latar belakang berhasil diatur - Latar belakang khusus dihapus - Ikon Alternatif - Ubah ikon peluncur menjadi ikon KernelSU. - Ikon diubah - - Sembunyikan Fungsi KPM - Menyembunyikan informasi dan fungsi KPM di layar utama dan panel bawah - - Pilih Mesin WebUI untuk Digunakan - Pilih Otomatis - Paksa Gunakan WebUI X - Paksa Gunakan KSU WebUI - Sisipkan Eruda ke WebUI X - Sisipkan konsol debug ke WebUI X untuk memudahkan debugging. Memerlukan debugging web diaktifkan. - - DPI yang Diterapkan - Sesuaikan kepadatan layar hanya untuk aplikasi saat ini - Kecil - Sedang - Besar - Sangat Besar - Khusus - Menerapkan Pengaturan DPI - Konfirmasi Perubahan DPI - Apakah Anda yakin ingin mengubah DPI aplikasi dari %1$d menjadi %2$d? - Aplikasi perlu dijalankan ulang agar pengaturan DPI baru diterapkan; ini tidak akan mempengaruhi bilah status sistem atau aplikasi lainnya - DPI diatur ke %1$d, akan diterapkan setelah aplikasi dijalankan ulang - - Bahasa Aplikasi - Ikuti Sistem - Pengaturan Pencahayaan Kartu - - kode kesalahan - Silakan periksa log - Memasang modul %1$d/%2$d - Gagal memasang %d modul baru - Gagal mengunduh modul - Mem-flash Kernel - - Semua - Root - Khusus - Bawaan - - Nama Naik - Nama Turun - Waktu Pemasangan (Baru) - Waktu Pemasangan (Lama) - Ukuran Turun - Ukuran Naik - Frekuensi Penggunaan - - Tidak ada aplikasi dalam kategori ini - - Tolak Hak Akses - Berikan Hak Akses - Lepas Kaitan Modul - Nonaktifkan Lepas Kaitan Modul - Perluas Menu - Ciutkan Menu - Ke Atas - Ke Bawah - Terpilih - Pilih - - Opsi Menu - Urutkan Berdasarkan - Pilih Jenis Aplikasi - - Konfigurasi SuSFS - Deskripsi Konfigurasi - Fitur ini memungkinkan Anda untuk mengonfigurasi spoofing nilai uname dan waktu build SuSFS. Masukkan nilai yang diinginkan dan klik \"Terapkan\" agar berlaku. - Nilai Uname - Silakan masukkan nilai uname khusus - Spoof Waktu Build - Silakan masukkan nilai spoof waktu build - Nilai Saat Ini: %s - Waktu Build Saat Ini: %s - Atur Ulang ke Bawaan - Terapkan - - Konfirmasi Atur Ulang - - Gagal menemukan file ksu_susfs - Gagal mengeksekusi perintah SuSFS - Kesalahan eksekusi perintah SuSFS: %s - Nilai uname dan waktu build SuSFS berhasil diatur: %s, %s - - Konfigurasi SuSFS - - Mulai Otomatis - Secara otomatis menerapkan semua konfigurasi non-bawaan saat reboot - Perlu menambahkan konfigurasi untuk mengaktifkan - Gagal mengaktifkan mulai otomatis - Gagal menonaktifkan mulai otomatis - Kesalahan konfigurasi mulai otomatis: %s - Tidak ada konfigurasi yang tersedia untuk mulai otomatis - - Pengaturan Dasar - Jalur SUS - Kaitan SUS - Coba Lepas Kait - Pengaturan Jalur - Status Fitur Diaktifkan - - Tambah Jalur SUS - Tambah Kaitan SUS - Tambah Coba Lepas Kait - Jalur SUS berhasil ditambahkan - Kesalahan: Jalur tidak ditemukan - Jalur - Jalur Kaitan - misalnya: /system/addon.d - Tidak ada jalur SUS yang dikonfigurasi - Tidak ada kaitan SUS yang dikonfigurasi - Tidak ada coba lepas kait yang dikonfigurasi - - Mode Lepas Kait - Lepas Kait Normal (0) - Lepas Kait Terpisah (1) - Normal - Terpisah - Mode: %1$s (%2$s) - Jalur coba lepas kait berhasil ditambahkan: %s - Berhasil menyimpan jalur coba lepas kait: %s - - - Atur Ulang Jalur SUS - Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? - Atur Ulang Kaitan SUS - Ini akan menghapus semua konfigurasi kaitan SUS. Apakah Anda yakin ingin melanjutkan? - Atur Ulang Coba Lepas Kait - Ini akan menghapus semua konfigurasi coba lepas kait. Apakah Anda yakin ingin melanjutkan? - Atur Ulang Pengaturan Jalur - - Jalur Data Android - Jalur Kartu SD - Atur Jalur Data Android - Atur Jalur Kartu SD - - Menampilkan status saat ini dari fitur SuSFS yang diaktifkan - Informasi status fitur tidak ditemukan - Diaktifkan - Dinonaktifkan - - Dukungan Jalur SUS - Dukungan Kaitan SUS - Dukungan Coba Lepas Kait - Dukungan Spoof Uname - Spoof Cmdline/Bootconfig - Dukungan Open Redirect - Dukungan Logging - Kaitan Bawaan Otomatis - Kaitan Bind Otomatis - Coba Lepas Kaitan Bind Otomatis - Sembunyikan Simbol KSU SUSFS - Dukungan SUS Kstat - Fitur Toggle Mode SUS SU - - Fitur SuSFS yang Dapat Dikonfigurasi - Aktifkan Log SuSFS - Aktifkan atau nonaktifkan logging untuk SuSFS - Pengaturan Logging SuSFS - Mengaktifkan Logging SuSFS - Menonaktifkan Logging SuSFS - Perbarui JSON - URL Perbarui JSON disalin ke papan klip - - Tampilkan Informasi Modul Lebih Banyak - Tampilkan informasi modul tambahan seperti URL perbarui JSON - Lokasi Eksekusi - Lokasi Eksekusi Saat Ini: %s - Layanan - Post-FS-Data - Jalankan setelah layanan sistem dimulai - Jalankan setelah sistem file dikaitkan tetapi sebelum sistem sepenuhnya dinyalakan. Dapat menyebabkan bootloop - Informasi Slot - Lihat informasi slot boot saat ini dan salin nilainya - Slot Aktif Saat Ini: %s - Uname: %s - Waktu Build: %s - Saat Ini - Gunakan Uname - Gunakan Waktu Build - Gagal mendapatkan informasi slot - - Modul mulai otomatis SuSFS diaktifkan, jalur modul: %s - Modul mulai otomatis SuSFS dinonaktifkan - - Konfigurasi Kstat - Konfigurasi Kstat statis ditambahkan: %1$s - Konfigurasi Kstat dihapus: %1$s - Jalur Kstat ditambahkan: %1$s - Jalur Kstat dihapus: %1$s - Kstat diperbarui: %1$s - Klon Lengkap Kstat diperbarui: %1$s - Tambahkan Konfigurasi Kstat Statis - Jalur File/Direktori - Petunjuk: Anda dapat menggunakan \"default\" untuk menggunakan nilai asli - Tambah Jalur Kstat - Tambah - Atur Ulang Konfigurasi Kstat - Apakah Anda yakin ingin membersihkan semua konfigurasi Kstat? Tindakan ini tidak dapat dibatalkan. - Deskripsi Konfigurasi Kstat - • add_sus_kstat_statically: Informasi file/direktori statis - • add_sus_kstat: Tambahkan jalur sebelum bind mount, menjaga informasi asli - • update_sus_kstat: Perbarui ino target, membiarkan ukuran dan blok tidak berubah - • update_sus_kstat_full_clone: Perbarui hanya ino, membiarkan nilai asli lainnya - Konfigurasi Kstat Statis - Manajemen Jalur Kstat - Belum ada konfigurasi Kstat, klik tombol di atas untuk menambahkan - - Kontrol Penyembunyian Kaitan SUS - Kontrol perilaku penyembunyian kaitan SUS untuk proses - Sembunyikan Kaitan SUS untuk Semua Proses - Jika diaktifkan, kaitan SUS akan disembunyikan dari semua proses, termasuk proses KSU - Jika dinonaktifkan, kaitan SUS akan disembunyikan hanya dari proses non-KSU; proses KSU akan dapat melihat kaitan - Mengaktifkan penyembunyian kaitan SUS untuk semua proses - Menonaktifkan penyembunyian kaitan SUS untuk semua proses - Disarankan untuk mengatur ke nonaktif setelah layar terbuka atau pada tahap service.sh atau boot-completed.sh, karena ini seharusnya memperbaiki masalah dengan beberapa aplikasi root yang bergantung pada kaitan yang dibuat oleh proses KSU - Pengaturan Saat Ini: %s - Sembunyikan untuk Semua Proses - Sembunyikan Hanya untuk Proses Non-KSU - Mode Versi Kernel Sederhana - Aktifkan atau nonaktifkan tampilan versi kernel SukiSU sederhana - Jalur Data Android diatur ke: %s - Jalur Kartu SD diatur ke: %s - Pengaturan jalur mungkin tidak sepenuhnya berhasil, tetapi jalur SUS akan tetap ditambahkan - - Cadangan - Buat cadangan semua konfigurasi SuSFS. File cadangan akan menyertakan semua pengaturan, jalur, dan konfigurasi. - Buat Cadangan - Berhasil membuat cadangan: %s - Gagal membuat cadangan: %s - File cadangan tidak ditemukan - Format file cadangan tidak valid - Versi cadangan tidak cocok, tetapi akan dicoba untuk dipulihkan - Pulihkan - Pulihkan konfigurasi SuSFS dari file cadangan. Ini akan menimpa semua pengaturan saat ini. - Pilih File Cadangan - Konfigurasi berhasil dipulihkan dari cadangan yang dibuat %s pada perangkat: %s - Gagal memulihkan: %s - Konfirmasi Pemulihan - Ini akan menimpa semua konfigurasi SuSFS saat ini. Apakah Anda yakin ingin melanjutkan? - Pulihkan - Tanggal Cadangan: %s - Perangkat: %s - Versi: %s - Status Terkunci - Timpa properti status bootloader dalam layanan late_start - Bersihkan Sisa-sisa - Bersihkan file dan direktori sisa dari berbagai modul dan alat (dapat menyebabkan penghapusan yang tidak disengaja, kehilangan data, dan gagal boot, gunakan dengan hati-hati) - diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 64027ac4..87e5e4db 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -4,14 +4,16 @@ Tidak terinstal Klik untuk menginstal Berfungsi - Versi: %s + Versi: %d + SuperUser: %d + Modul: %d Tidak didukung - KernelSU saat ini hanya mendukung kernel GKI - Kernel - Versi SuSFS - Versi manager + KernelSU saat ini hanya mendukung kernel GKI. + Versi kernel + Versi manajer + Identitas Status SELinux - Nonaktif + Dinonaktifkan Enforcing Permisif Tidak diketahui @@ -20,14 +22,12 @@ Gagal menonaktifkan modul: %s Tidak ada modul yang terpasang Modul - Urut (Tindakan pertama) - Urut (Diaktifkan terlebih dahulu) Hapus Instal Instal Reboot - Pengaturan - Soft Reboot + Setelan + Reboot lunak Reboot ke Recovery Reboot ke Bootloader Reboot ke Download @@ -38,6 +38,7 @@ Gagal menghapus: %s Versi Oleh + Modul tidak tersedia karena kernel tidak mendukung OverlayFS! Muat ulang Tampilkan aplikasi sistem Sembunyikan aplikasi sistem @@ -47,21 +48,25 @@ Konflik dengan Magisk, fungsi modul ditiadakan! Pelajari KernelSU https://kernelsu.org/id_ID/guide/what-is-kernelsu.html - Pelajari cara instal KernelSU dan menggunakan modul + Pelajari cara menginstal KernelSU dan menggunakan modul. Dukung Kami KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan. - Gabung dengan kami di saluran %2$s]]> + Gabung kanal %2$s kami]]> + Profil Apl Bawaan Templat Khusus Nama profil + Gunakan Namespace + Diwariskan + Universal + Individual Kelompok Kemampuan Konteks SELinux Umount Modul Gagal membarui Profil pada %s - Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manager dengan baik. Harap tingkatkan ke versi %s atau yang lebih tinggi! - Melepas Modul secara bawaan + Lepaskan modul Menggunakan \"Umount Modul\" secara universal pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil. Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini. Domain @@ -69,629 +74,84 @@ Membarui Mengunduh modul: %s Mulai mengunduh: %s - Tersedia versi terbaru %s, Klik untuk membarui. + Versi baru %s tersedia, klik untuk memperbarui! Jalankan - Paksa berhenti + Paksa berhenti Mulai ulang Gagal membarui aturan SELinux pada: %s + Versi KernelSU %d terlalu rendah agar manajer berfungsi normal. Harap membarui ke versi %d atau di atasnya! Catatan Perubahan - Templat Profil Aplikasi - Atur templat Profil yang lokal dan daring - Buat templat - Edit templat - ID - ID template tidak valid - Nama - Deskripsi - Simpan - Hapus - Lihat templat - readonly - ID templat sudah ada! - Impor/Ekspor - Impor dari papan klip + Berhasil diimpor Ekspor ke papan klip Tidak ditemukan templat lokal untuk diekspor! - Berhasil diimpor - Sinkronkan templat daring - Gagal menyimpan templat - Papan klip kosong! + ID templat sudah ada! + Impor dari papan klip Gagal mengambil Changelog: %s - Cek terbaru - Cek terbaru setiap membuka aplikasi - Gagal memberikan akses root! - Tindakan - Tutup - Pengawakutuan WebView - Dapat digunakan untuk men-debug WebUI. Harap aktifkan hanya bila diperlukan. - Instal langsung (rekomendasi) - Pilih berkas - Instal ke slot nonaktif (setelah OTA) + Nama + ID templat tidak valid + Sinkronkan templat daring + Buat templat + Impor/Ekspor + Gagal menyimpan templat + Edit templat + ID + Templat Profil Aplikasi + Deskripsi + Simpan + Kelola templat lokal dan online dari Profil Aplikasi. + Hapus + Papan klip kosong! + Lihat templat + readonly + Debugging WebView + Dapat digunakan untuk mendebug antarmuka web (WebUI). Harap aktifkan hanya saat diperlukan. + %1$s image partisi terekomendasi + Pilih KMI + Selanjutnya Gawai akan **DIPAKSA** untuk but ke slot nonaktif! \nHANYA gunakan setelah proses OTA selesai. \nLanjutkan? - Selanjutnya + Instal langsung (rekomendasi) + Pilih berkas + Instal ke slot nonaktif (setelah OTA) + Gagal memberikan akses root! + Buka + Periksa pembaruan + Periksa pembaruan secara otomatis saat membuka aplikasi. + Menghapus KernelSU (root dan semua modul) secara lengkap dan permanen. + Hapus sementara + Pulihkan image bawaan + Hapus + Sementara menghapus KernelSU, memulihkan ke kondisi asal setelah reboot berikutnya. + Hapus permanen + Pulihkan image pabrik asli (jika ada cadangan), biasanya digunakan sebelum pembaruan OTA; jika Anda perlu menghapus KernelSU, silakan gunakan \"Hapus secara permanen\". + Pemasangan Berhasil + LKM dipilih: %s + Pasang + Pemasangan Gagal + Simpan Log + Aksi + Log disimpan + Urut (Aktif pertama) + Urut (Tindakan pertama) + Modul yg akan diinstal: %1$s + Oke + Akses SU tidak dapat diberikan ke %s + Nonaktifkan kompatibilats SU + Nonaktifkan kemampuan aplikasi apa pun untuk mendapatkan hak akses root melalui perintah `su` (proses root yang sudah ada tidak akan terpengaruh). + Periksa pembaruan modul Gunakan berkas LKM lokal Hanya berkas .ko yang didukung - %1$s image partisi terekomendasi - Pilih KMI - Hapus - Hapus sementara - Hapus permanen - Pulihkan image bawaan - Sementara menghapus KernelSU, memulihkan ke kondisi asal setelah reboot berikutnya. - Hapus permanen KernelSU (root dan modul). - Pulihkan image bawaan ROM (jika cadangan tersedia), umumnya dilakukan sebelum OTA; jika ingin menghapus KernelSU, gunakan fungsi \"Hapus permanen\". - Pasang - Pemasangan Berhasil - Pemasangan Gagal - LKM dipilih: %s - Simpan Log - Log disimpan - - konfirmasi pemasangan modul %1$s? - module tidak dikenal - - Konfirmasi pemulihan module - Operasi ini akan menimpa semua modul yang ada. Lanjutkan? - Konfirmasi - Batal - - Pencadangan berhasil (tar.gz) - Pencadangan gagal: %1$s - cadangkan modul - pulihkan modul - - Modul berhasil dipulihkan, restart diperlukan - Pemulihan gagal: %1$s - Mulai Ulang Sekarang - Kesalahan tidak diketahui - - Eksekusi perintah gagal: %1$s - - Cadangan daftar izin berhasil - Gagal mencadangkan daftar izin: %1$s - Konfirmasi Pemulihan Daftar Izin - Operasi ini akan menimpa daftar izin saat ini. Lanjutkan? - Daftar izin berhasil dipulihkan - Gagal memulihkan daftar izin: %1$s - Cadangkan Daftar Izin - Pulihkan Daftar Izin - Latar belakang kustom - Pilih gambar untuk latar belakang - NavBar transparant - Versi Android - Model Perangkat - Memberikan hak superuser kepada %s tidak diizinkan - Nonaktifkan kompatibilitas SU - Nonaktifkan sementara kemampuan aplikasi untuk mendapatkan hak akses root melalui perintah ⁠su (proses root yang sedang berjalan tidak akan terpengaruh) - Nonaktifkan pelepasan (unmount) kernel - Nonaktifkan perilaku unmount pada level kernel yang digunakan oleh KernelSU. + Nonaktifkan umount kernel + Nonaktifkan perilaku umount tingkat kernel yang dikendalikan oleh KernelSU. Aktifkan keamanan yang ditingkatkan Aktifkan kebijakan keamanan yang lebih ketat. - Bawaan + Default Aktifkan sementara - Aktifkan secara permanen - Apakah Anda yakin ingin menginstal %1$d modul berikut?\n\n%2$s - Setelan lainnya - Selinux - Aktifkan - Nonaktifkan - Mode simple - Sembunyikan papan kartu di beranda - Sembunyikan versi kernel - Sembunyikan versi kernel jika namanya tidak yakin - Sembunyikan info lain - Sembunyikan notifikasi titik merah (jumlah Super User, modul, dan modul KPM) di bilah navigasi - Sembunyikan status SuSFs - Sembunyikan status susfs di halaman awal beranda - Sembunyikan status zygisk - Sembunyikan informasi implementasi Zygisk di halaman utama - Sembunyikan kartu tautan - Sembunyikan papan kartu URL di halaman awal beranda - Sembunyikan baris label modul - Sembunyikan label nama folder dan ukuran di kartu modul - Tema - Mengikuti sistem - Terang - Hitam - Hook manual - Warna dinamik - Warna dinamik, menggunakan sistem tema - Pilih warna tema - Biru - Hijau - Ungu - Oren - Ping - Abu - Kuning - Memasang Anykernel3 - Memasang file kernel AnyKernel3 - Butuh izin root - Pembersihan selesai - Apakah ingin restart sekarang? - Iya - Tidak - Mulai ulang gagal - KPM - Tidak ada modul kernel yang terpasang saat ini - Versi - Pembuat - Uninstal - Berhasil di Uninstal - Gagal Uninstal - Memuat module KPM berhasil - Memuat module KPM gagal! - Parameter - Eksekusi - Versi KPM - Tutup - Fungsi-fungsi modul kernel berikut dikembangkan oleh KernelPatch dan dimodifikasi untuk menyertakan fungsi modul kernel dari SukiSU Ultra - Antusias Untuk SukiSU Ultra - Sukses - Gagal - SukiSU Ultra akan menjadi cabang KSU yang relatif independen di masa mendatang, tetapi kami tetap menghargai KernelSU dan MKSU resmi dan sebagainya atas kontribusi mereka! - Tidak Mendukung - Mendukung - Kernel belum ditambal - Kernel belum dikonfigurasi - Pengaturan kostum - Instalasi KPM - Muat - Sematkan - Silakan pilih: %1\$s Mode Instalasi Modul \n\nMuat: Memuat sementara modul \nSematkan: Menginstal secara permanen ke dalam sistem - Gagal memeriksa keberadaan file modul - Warna Tema - Format file tidak sesuai. Silakan pilih file dengan format .kpm. - Menghapus instalan - KPM berikut akan diuninstall: %s - Gunakan dua jari untuk memperbesar gambar, dan satu jari untuk menggeser mengatur posisi - Reprovisi - - Flash Selesai - - Mempersiapkan… - Membersihkan Berkas... - Menyalin file... - Mengekstrak alat flash… - Memperbaiki skrip flash… - Mem-flash kernel… - Flash selesai - - Pilih Slot Flash - Silakan pilih slot target untuk flash boot - Slot A - Slot B - Slot yang dipilih: %1$s - Mendapatkan slot asli - Mengatur slot yang ditentukan - Pulihkan Slot Default - Slot default sistem saat ini:%1$s - - Menyalin gagal - Kesalahan yang tidak diketahui - Flash gagal - - Perbaikan/pemasangan LKM - Mem-flash AnyKernel3 - Versi kernel: %1$s - Menggunakan alat perbaikan:%1$s - Konfigurasi - Pengaturan Aplikasi - Alat-Alat - - Aplikasi tidak ditemukan - SELinux Dinyalakan - SELinux Dimatikan - Perubahan Status SELinux Gagal - Pengaturan Lanjutan - Kustomisasi toolbar - Kembali - Set latar belakang berhasil - Latar belakang khusus yang dihapus - Ubah ikon - Ubah ikon peluncur aplikasi ke ikon KernelSU - Ikon dirubah - - Tampilkan fungsi KPM - Tampilkan fungsi informasi KPM dan menu KPM di bilah navigasi - - Pilih jenis webUI untuk digunakan - Otomatis memilih - Paksa menggunakan WebUI X - Penggunaan wajib KSU WebUI - Suntik Eruda ke WebUI X - Suntikkan konsol debug ke dalam WebUI X untuk mempermudah proses debugging. Memerlukan pengaktifan web debugging. - - Ubah DPI - Pengaturan DPI hanya untuk aplikasi ini saja - Kecil - Sedang - Besar - Jumbo - Kustomisasi - Terapkan setelan DPI - Konfirmasi perubahan DPI - Apa kamu yakin ingin merubah DPI aplikasi dari %1$d ke %2$d? - Aplikasi membutuhkan restar untuk menerapkan opsi DPI ini, perubahan ini tidak mengganggu DPI sistem - DPI telah di rubah ke %1$d, efektif setelah aplikasi di restar - - Bahasa Aplikasi - Mengikuti sistem - Penyesuaian Kegelapan Kartu - - Kode error - Silahkan periksa log - Modul yang dipasang %1$d/%2$d - %d Gagal memasang modul baru - Download modul gagal - Memasang Kernel - - Semua - Akar - Kostum - Bawaan - - Urutan naik nama - Urutan turun nama - Waktu pemasangan (baru) - Waktu pemasangan (lama) - Urutan turun ukuran - Urutan naik ukuran - Frekuensi penggunaan - - Tidak ada aplikasi dalam kategori ini - - Penolakan otorisasi - Otorisasi - Melepas Pemasangan Modul - Nonaktifkan pelepasan pemasangan modul - Luaskan menu - Tutup menu - Atas - Bawah - Dipilih - pilihan - - Opsi Menu - Urut berdasarkan - Pilihan Jenis Aplikasi - - Konfigurasi SuSFS - Deskripsi Konfigurasi - Fitur ini memungkinkan Anda menyesuaikan nilai uname SuSFS dan spoofing waktu build. Masukkan nilai yang ingin Anda atur lalu klik Terapkan untuk memproses perubahan. - Nilai Uname - Silakan masukkan nilai uname khusus - Spoofing Waktu membangun - Masukkan nilai spoofing waktu membangun - Nilai saat ini: %s - Waktu membangun saat ini: %s - Setel Ulang ke Default - Terapkan - - Konfirmasi Setel Ulang - - File ksu_susfs tidak ditemukan - Eksekusi perintah SUSFS gagal - Gagal menjalankan perintah SUSFS: %s - Berhasil atur uname dan waktu build SUSFS: %s, %s - - Konfigurasi SUSFS - - Mulai Otomatis - Terapkan semua konfigurasi non-default secara otomatis saat mulai ulang - Perlu tambahan konfigurasi untuk mengaktifkan - Gagal mengaktifkan mulai otomatis - Gagal menonaktifkan mulai otomatis - Kesalahan konfigurasi mulai otomatis: %s - Tidak ada konfigurasi yang tersedia untuk mulai otomatis - - Pengaturan Dasar - Jalur SUS - Pemasangan SUS - Coba Umount - Pengaturan Path - Status Fitur yang Diaktifkan - - Tambahkan Jalur SUS - Tambahkan Pemasangan SUS - Tambahkan Coba Umount - Jalur SUS berhasil ditambahkan - Kesalahan jalur tidak ditemukan - Jalur - Jalur Pemasangan - contoh: /system/addon.d - Tidak ada jalur SUS yang dikonfigurasi - Tidak ada pemasangan SUS yang dikonfigurasi - Tidak ada coba umount yang dikonfigurasi - - Mode Umount - Umount Normal (0) - Umount Lepas (1) - Normal - Lepas - Mode: %1$s (%2$s) - Jalur coba umount berhasil ditambahkan: %s - Jalur coba umount berhasil disimpan: %s - - - Setel Ulang Jalur SUS - Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? - Setel Ulang Pemasangan SUS - Ini akan menghapus semua konfigurasi mount SUS. Apakah Anda yakin ingin melanjutkan? - Setel Ulang Coba Umount - Ini akan menghapus semua konfigurasi umount. Apakah Anda yakin ingin melanjutkan? - Setel Ulang Pengaturan Jalur - - Jalur Data Android - Jalur SD Card - Atur Jalur Data Android - Atur Jalur SD Card - - Tampilkan status fitur SuSFS yang saat ini diaktifkan - Tidak ditemukan informasi status fitur - Diaktifkan - Dinonaktifkan - - Dukungan Jalur SUS - Dukungan Pemasangan SUS - Dukungan Coba Umount - Dukungan Spoof uname - Spoof Cmdline/Bootconfig - Dukungan Pengalihan Terbuka - Dukungan Logging - Pemasangan Default Otomatis - Pemasangan Bind Otomatis - Coba Umount Bind Mount Otomatis - Sembunyikan Simbol KSU SUSFS - Dukungan SUS Kstat - Fungsi pengalihan mode SUS SU - - Fitur SuSFS yang Dapat Dikonfigurasi - Aktifkan Log SuSFS - Aktifkan atau nonaktifkan logging untuk SuSFS - Konfigurasi Logging SuSFS - Mengaktifkan Logging SuSFS - Menonaktifkan logging SuSFS - Perbarui JSON - URL Pembaruan JSON disalin ke papan klip - - Tampilkan info modul lainnya - Pajang info modul tambahan seperti URL pembaruan JSON - Lokasi Eksekusi - Lokasi eksekusi saat ini: %s - Layanan - Post-FS-Data - Eksekusi setelah layanan sistem dimulai - Eksekusi setelah sistem file dipasang tetapi sebelum sistem sepenuhnya boot, Dapat menyebabkan boot loop - Informasi Slot - Lihat informasi slot boot saat ini dan salin nilai - Slot Aktif Saat Ini: %s - Uname: %s - Waktu Build: %s - Saat Ini - Gunakan Uname - Gunakan Waktu Build - Tidak dapat mengambil informasi slot - - Modul autostart SuSFS diaktifkan, jalur modul: %s - Modul autostart SuSFS dinonaktifkan - - Konfigurasi Kstat - Konfigurasi statis Kstat ditambahkan: %1$s - Konfigurasi Kstat dihapus: %1$s - Jalur Kstat ditambahkan: %1$s - Jalur Kstat dihapus: %1$s - Kstat diperbarui: %1$s - Kstat full clone diperbarui: %1$s - Tambahkan Konfigurasi Statis Kstat - Jalur File/Direktori - Petunjuk: Anda dapat menggunakan ”default“ untuk menggunakan nilai asli - Tambahkan Jalur Kstat - Tambahkan - Setel Ulang Konfigurasi Kstat - Apakah Anda yakin ingin menghapus semua konfigurasi Kstat? Tindakan ini tidak dapat dibatalkan. - Deskripsi Konfigurasi Kstat - • add_sus_kstat_statically: Info stat statis file/direktori - • add_sus_kstat: Tambahkan jalur sebelum bind mount, menyimpan info stat asli - • update_sus_kstat: Perbarui target ino, pertahankan ukuran dan blok tidak berubah - • update_sus_kstat_full_clone: Perbarui ino saja, pertahankan nilai asli lainnya - Konfigurasi Statis Kstat - Manajemen Jalur Kstat - Belum ada konfigurasi Kstat, klik tombol di atas untuk menambahkan - - Kontrol Penyembunyian Pemasangan SUS - Kontrol perilaku penyembunyian pemasangan SUS untuk proses - Sembunyikan pemasangan SUS untuk semua proses - Saat diaktifkan, pemasangan SUS akan disembunyikan dari semua proses, termasuk proses KSU - Saat dinonaktifkan, pemasangan SUS hanya akan disembunyikan dari proses non-KSU, proses KSU dapat melihat pemasangan - Mengaktifkan penyembunyian pemasangan SUS untuk semua proses - Menonaktifkan penyembunyian pemasangan SUS untuk semua proses - Disarankan untuk menonaktifkan setelah layar tidak terkunci, atau selama tahap service.sh atau boot-completed.sh, karena ini seharusnya memperbaiki masalah pada beberapa aplikasi root yang bergantung pada pemasangan yang dipasang oleh proses KSU - Pengaturan saat ini: %s - Sembunyikan untuk semua proses - Sembunyikan hanya untuk proses non-KSU - Mode Ringkas Versi Kernel - Aktifkan atau nonaktifkan mode bersih yang ditampilkan oleh versi kernel SukiSU - Jalur Data Android telah diatur ke: %s - Jalur SD card telah diatur ke: %s - Penyiapan jalur mungkin tidak sepenuhnya berhasil, tetapi jalur SUS akan terus ditambahkan - - Backup - Buat backup dari semua konfigurasi SuSFS. File backup akan mencakup semua pengaturan, jalur, dan konfigurasi. - Buat Backup - Backup berhasil dibuat: %s - Pembuatan backup gagal: %s - File backup tidak ditemukan - Format file backup tidak valid - Versi backup tidak cocok, tetapi akan mencoba memulihkan - Pulihkan - Pulihkan konfigurasi SuSFS dari file backup. Ini akan menimpa semua pengaturan saat ini. - Pilih File Backup - Konfigurasi berhasil dipulihkan dari backup yang dibuat pada %s dari perangkat: %s - Pemulihan gagal: %s - Konfirmasi Pemulihan - Ini akan menimpa semua konfigurasi SuSFS saat ini. Apakah Anda yakin ingin melanjutkan? - Pulihkan - Tanggal Backup: %s - Perangkat: %s - Versi: %s - Status kunci - Timpa atribut status penguncian bootloader dalam mode layanan late_start - Bersihkan Residu - Bersihkan file dan direktori sisa dari berbagai modul dan alat (mungkin terhapus secara tidak sengaja, mengakibatkan kehilangan dan gagal memulai, gunakan dengan hati-hati) - Edit Jalur SUS - Edit Pemasangan SUS - Edit Coba Umount - Edit Konfigurasi Statis Kstat - Edit Jalur Kstat - Simpan - Edit - Hapus - Perbarui - Pembaruan konfigurasi Kstat - Pembaruan jalur Kstat - Pembaruan full clone Susfs - Lepas Layanan Isolasi Zygote - Aktifkan opsi ini untuk melepaskan titik pemasangan layanan isolasi Zygote saat sistem mulai - Lepas layanan isolasi Zygote diaktifkan - Lepas layanan isolasi Zygote dinonaktifkan - Jalur Aplikasi - Jalur lainnya - Lainnya - Aplikasi - Tambahkan Jalur Aplikasi - Versi pustaka SuSFS tidak cocok, kernel: %1$s vs manajer: %2$s. Disarankan untuk memperbarui kernel atau manajer - Peringatan - Cari Aplikasi - %1$d aplikasi dipilih - %1$d aplikasi sudah ditambahkan - Semua aplikasi telah ditambahkan - Konfigurasi Tanda Tangan Dinamis - Diaktifkan (Ukuran: %s) - Dinonaktifkan - Aktifkan Tanda Tangan Dinamis - Ukuran Tanda Tangan - Hash Tanda Tangan - Hash harus 64 karakter heksadesimal - Konfigurasi tanda tangan dinamis berhasil diatur - Gagal mengatur konfigurasi tanda tangan dinamis - Konfigurasi tanda tangan tidak valid - Tanda tangan dinamis dinonaktifkan - Gagal membersihkan tanda tangan dinamis - Dinamis - Tanda Tangan %1$d - Tidak diketahui - Manajer Aktif - Tidak ada manajer aktif - SukiSU - Implementasi Zygisk - - Jalur Loop SUS - Tambahkan Jalur Loop SUS - Edit Jalur Loop SUS - Jalur loop SUS berhasil ditambahkan: %1$s - Jalur loop SUS dihapus: %1$s - Jalur loop SUS diperbarui: %1$s -> %2$s - Tidak ada jalur loop SUS yang dikonfigurasi - Setel Ulang Jalur Loop - Apakah Anda yakin ingin menghapus semua jalur loop SUS? Tindakan ini tidak dapat dibatalkan. - Jalur Loop - /data/contoh/jalur - Catatan: Hanya jalur TIDAK di dalam /storage/ dan /sdcard/ yang dapat ditambahkan melalui jalur loop. - Kesalahan: Jalur loop tidak dapat berada di dalam direktori /storage/ atau /sdcard/ - Jalur Loop - Tambahkan Jalur Loop - - Konfigurasi Jalur Loop - Jalur loop ditandai ulang sebagai SUS_PATH pada setiap startup aplikasi pengguna non-root atau layanan terisolasi. Ini membantu mengatasi masalah di mana jalur yang ditambahkan mungkin memiliki status inode direset atau inode dibuat ulang di kernel. - Palsukan log AVC - Palsukan log AVC telah diaktifkan - Palsukan log AVC telah dinonaktifkan - Dinonaktifkan: Nonaktifkan pemalsuan sus tcontext dari \'su\' yang ditampilkan di avc log di kernel\n -Diaktifkan: Aktifkan pemalsuan sus tcontext dari \'su\' dengan \'kernel\' yang ditampilkan di avc log in kernel - Catatan Penting:\n -- Secara default pada kernel nilai ini disetel ke \'0\'\n -- Mengaktifkan ini terkadang membuat pengembang lebih sulit mengidentifikasi penyebab saat melakukan debugging terkait izin atau masalah SELinux, sehingga disarankan agar pengguna menonaktifkannya saat sedang melakukan debugging - - Tervalidasi - Tanda tangan modul tervalidasi - Verifikasi Tanda Tangan - Verifikasi tanda tangan secara paksa saat modul dipasang. (Hanya tersedia untuk arsitektur ARM) - Penerbit tidak dikenal - Modul yang tidak ditandatangani mungkin tidak lengkap. Untuk melindungi perangkat Anda, pemasangan modul ini diblokir. - Modul yang tidak ditandatangani mungkin tidak lengkap. Apakah Anda ingin mengizinkan modul berikut dari penerbit tidak dikenal untuk dipasang di perangkat ini? - Jenis hook - - Patch KPM - Untuk menambahkan fitur KPM tambahan - Patch KPM - Terapkan patch KPM ke image kernel sebelum melakukan flashing - Batalkan Patch KPM - Batalkan patch KPM yang telah diterapkan sebelumnya - Patch KPM aktif - Pembatalan patch KPM diaktifkan - Mode Patch KPM - Mode Pembatalan Patch KPM - - Sedang menyiapkan Alat KPM - Menerapkan patch KPM - Membatalkan patch KPM - Menemukan berkas Image: %s - KPM berhasil diterapkan - Patch KPM berhasil dibatalkan - File berhasil direpack - - Gagal mengekstrak berkas zip - Berkas Image tidak ditemukan - Patch KPM gagal - Pembatalan patch KPM gagal - Operasi patch KPM gagal: %s - - Ikuti kernel - Gunakan kernel apa adanya tanpa perubahan dari KPM - - Daftar aplikasi pemindaian pada mode pengguna - Mengaktifkan opsi ini akan menggunakan pemindaian mode pengguna untuk daftar aplikasi, sehingga meningkatkan kestabilan. (Jika Anda mengalami masalah seperti hang saat kernel memindai daftar aplikasi, Anda dapat mencoba mengaktifkan opsi ini.) - Pemindaian Aplikasi Multi-Pengguna - Ketika diaktifkan, fitur ini akan memindai aplikasi untuk semua pengguna, termasuk profil kerja - Gagal mengatur, silakan periksa perizinan - Bersihkan Lingkungan Runtime - Bersihkan berkas runtime dan hentikan layanan pemindai - Apakah Anda yakin ingin membersihkan lingkungan runtime? Tindakan ini akan menghentikan layanan pemindai dan menghapus berkas yang terkait. - Lingkungan runtime berhasil dibersihkan - Gagal membersihkan lingkungan runtime - - Konfirmasi Instalasi - Konfirmasi Instalasi (Berkas %d) - Instal - Modul - Kernel - Tidak diketahui - Kernel tidak diketahui - Berkas tidak diketahui - Versi - Pembuat - Deskripsi - Perangkat yang didukung - - Peta SUS - Jalur Pustaka - /data/adb/modules/my_module/zygisk/arm64-v8a.so - Tambahkan Peta SUS - Sunting Peta SUS - Peta SUS berhasil ditambahkan: %1$s - Peta SUS telah dihapus: %1$s - Peta SUS telah diperbarui: %1$s -> %2$s - Tidak ada peta SUS yang dikonfigurasi - Atur ulang Peta SUS - Tindakan ini akan menghapus semua peta SUS yang telah dikonfigurasi. Tindakan ini tidak dapat dibatalkan. - Penyembunyian Peta Memori - Sembunyikan berkas nyata yang di-mmapped dari berbagai peta di /proc/self/ - - Cari - Bersihkan Log - Apakah Anda yakin ingin mengosongkan berkas log yang dipilih? Tindakan ini tidak dapat dibatalkan. - + Aktifkan permanen + Sedang diproses… + Tarik ke bawah untuk menyegarkan + Lepaskan untuk menyegarkan + Menyegarkan… + Penyegaran berhasil diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml index a33f66cb..f601e5ae 100644 --- a/manager/app/src/main/res/values-it/strings.xml +++ b/manager/app/src/main/res/values-it/strings.xml @@ -4,12 +4,14 @@ Non installato Clicca per installare In esecuzione - Versione: %s + Versione: %d + Applicazioni con accesso root: %d + Moduli installati: %d Non supportato KernelSU ora supporta solo i kernel GKI Kernel - SuSFS Version Versione del manager + Impronta della build di Android Stato di SELinux Disabilitato Enforcing @@ -20,8 +22,6 @@ Impossibile disabilitare il modulo: %s Nessun modulo installato Modulo - Sort (Action first) - Sort (Enabled first) Disinstalla Installa Installa @@ -38,6 +38,7 @@ Impossibile disinstallare: %s Versione Autore + overlayfs non è disponibile, i moduli non possono funzionare! Ricarica Mostra app di sistema Nascondi app di sistema @@ -50,41 +51,40 @@ Scopri come installare KernelSU e utilizzare i moduli Supportaci KernelSU è, e sempre sarà, gratuito e open source. Puoi comunque mostrarci il tuo apprezzamento facendo una donazione. - Join our %2$s channel]]> - Predefinito - Modello - Personalizzato + Unisciti al nostro canale %2$s]]> Nome profilo + Spazio dei nomi del mount + Globale Gruppi - Capacità - Contesto SELinux + Ereditato + Individuale + Predefinito + Personalizzato + Modello Scollega moduli + Contesto SELinux Aggiornamento App Profile per %s fallito - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Scollega moduli da default - Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. - Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. - Dominio - Regole Aggiorna + Apri + Capacità + Scollega moduli da default + Regole Sto scaricando il modulo: %s Inizia a scaricare:%s Nuova versione: %s disponibile, tocca per aggiornare - Apri - Arresto forzato + Arresto forzato Riavvia Aggiornamento regole SELinux per %s fallito + Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. + Dominio + Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. + La versione attualmente installata di KernelSU (%d) è troppo vecchia ed il gestore non può funzionare correttamente. Si prega di aggiornare alla versione %d o successiva! Registro aggiornamenti - Modelli App Profile - Gestisci i modelli locali e remoti di App Profile Crea modello Modifica modello identificatore Identificativo modello non valido Nome - Descrizione - Salva - Elimina Visualizza modello Sola lettura L\'identificatore del modello è già in uso! @@ -94,271 +94,39 @@ Impossibile trovare un modello locale da esportare! Importato con successo Sincronizza i modelli remoti - Impossibile salvare il modello Gli appunti sono vuoti! + Impossibile ottenere l\'accesso root! + Modelli App Profile + Gestisci i modelli locali e remoti di App Profile + Elimina + Descrizione + Salva + Impossibile salvare il modello + Apri Impossibile reperire il changelog: %s Controlla aggiornamenti Controlla automaticamente la disponibilità di aggiornamenti all\'apertura dell\'applicazione - Impossibile ottenere l\'accesso root! - Action - Close Abilita il debug di WebView Può essere usato per svolgere il debug di WebUI, è consigliato attivarlo solo quando necessario. + È consigliato usare immagine della partizione %1$s + Scegli il KMI + Avanti Installazione diretta (Raccomandata) Scegli un file Installa nello slot inattivo (dopo OTA) Il tuo dispositivo sarà **FORZATO** ad avviarsi nello slot inattivo dopo il riavvio! \nUsa questa opzione solo quando l\'applicazione dell\'aggiornamento OTA è terminata. \nProcedere? - Avanti - È consigliato usare immagine della partizione %1$s - Scegli il KMI Disinstalla Disinstalla temporaneamente Disinstalla permanentemente Ripristina immagine originale del produttore Disinstalla temporaneamente KernelSU, ripristina lo stato originale dopo il prossimo riavvio. Disinstalla KernelSU (root e tutti i moduli) completamente e permanentemente. - Ripristina l\'immagine di fabbrica del produttore (se il backup è presente), solitamente usato prima di applicare l\'OTA; se devi disinstallare KernelSU, utilizza invece \"Disinstalla permanentemente\". Installazione Installazione completata Installazione fallita LKM selezionato: %s + Ripristina l\'immagine di fabbrica del produttore (se il backup è presente), solitamente usato prima di applicare l\'OTA; se devi disinstallare KernelSU, utilizza invece \"Disinstalla permanentemente\". Salva Registri - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - diff --git a/manager/app/src/main/res/values-iw/strings.xml b/manager/app/src/main/res/values-iw/strings.xml index 7860f80a..51ad608f 100644 --- a/manager/app/src/main/res/values-iw/strings.xml +++ b/manager/app/src/main/res/values-iw/strings.xml @@ -9,37 +9,46 @@ שלח לוג מושבת תמכו בנו + ירושה מודולים מושבתים מכיוון שהם מתנגשים עם זה של Magisk! יומן שינויים התרים הפעלה מחדש למצב הורדה טעינת מודולים כברירת מחדל הפעלת אפשרות זו תאפשר ל-KernelSU לשחזר קבצים שהשתנו על ידי המודולים עבור יישום זה. + אישי הפעלת המודל נכשלה: %s עצירה בכח - הפעלה מחדש למצב EDL + הפעלה מחדש למצב EDL איתחול יכולת + משתמשי על: %d מפעיל מודל: %s + גלובלי ערך ברירת המחדל הגלובלי עבור \"טעינת מודולים\" בפרופילי אפליקציה. אם מופעל, זה יסיר את כל שינויי המודול למערכת עבור יישומים שאין להם ערכת פרופיל. + מודלים:%d אכיפה הקשר SELinux + טביעת אצבע ברירת מחדל להשיק מצב בטוח + גרסת KernelSU הנוכחית %d נמוכה מדי כדי שהמנהל יפעל כראוי. אנא שדרג לגרסה %d ומעלה! הפעלה מחדש לריקברי רך Reboot שם פרופיל KernelSU הוא, ותמיד יהיה, חינמי וקוד פתוח. עם זאת, תוכל להראות לנו שאכפת לך על ידי תרומה. הסרה + טעינת מרחב שמות התקנה לחץ להתקנה כללים קבוצה + שכבות-על לא זמינות, המודול לא יכול לעבוד! מודולים יוצר אודות - גרסה: %s + גרסה: %d הפעלה מחדש KernelSU תומך רק בליבת GKI כעת סטטוס SELinux @@ -68,6 +77,7 @@ https://kernelsu.org/guide/what-is-kernelsu.html נכשל עדכון כללי SELinux עבור: %s הפעלה מחדש לבוטלאודר + ראה את קוד המקור ב%1$s
הצטרף אלינו %2$s בערוץ
גרסת מנהל גרסה חדשה עבור: %s זמינה, לחץ כדי לשדרג שמור יומנים diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index fdbfa061..fde9039b 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -4,612 +4,154 @@ 未インストール タップでインストール 動作中 - バージョン: %s + バージョン: %d + スーパーユーザー: %d + モジュール: %d 非対応 - カーネルの KernelSU ドライバが未検出です。カーネルが間違っていませんか? - カーネル バージョン - SuSFS バージョン - マネージャー バージョン - SELinux のステータス - 無効 + 現在、 KernelSU は GKI カーネルにのみ対応しています + カーネル + アプリのバージョン + Fingerprint + SELinux の状態 + Disabled Enforcing Permissive 不明 スーパーユーザー - %s モジュールを ON にできませんでした - %s モジュールを OFF にできませんでした + モジュールの有効化に失敗しました: %s + モジュールの無効化に失敗しました: %s モジュールがインストールされていません モジュール - 並べ替え (アクションを優先) - 並べ替え (最初に有効) アンインストール インストール インストール 再起動 設定 - ソフトリブート - リカバリーで再起動 - ブートローダーで再起動 - ダウンロードモードで再起動 - EDL で再起動 + 通常の再起動 + リカバリーへ再起動 + ブートローダー へ再起動 + ダウンロードモードへ再起動 + EDL へ再起動 アプリについて - %s モジュールをアンインストールしますか? - %s をアンインストールしました - %s をアンインストールできませんでした + モジュール %s をアンインストールしますか? + %s はアンインストールされました + アンインストールに失敗しました: %s バージョン - 作者 + 制作者 + カーネルによって OverlayFS が無効になっているため、モジュールが利用できません! 更新 システムアプリを表示 システムアプリを非表示 - ログを送信する + ログを送信 セーフモード 再起動すると有効化されます - モジュールが Magisk との競合により利用できません! - KernelSU について学ぶ + モジュールが Magisk との競合により利用できません! + KernelSU について https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html - KernelSU のインストール方法やモジュールの使い方を学習できます。 + KernelSU のインストール方法やモジュールの使い方はこちら 支援する - KernelSU は今後も無料でオープンソースです。ですが、寄付をして頂けると開発者への貢献になります。 - %2$s チャンネルにご参加ください

アニメキャラのスタンプ付き画像の著作権は%3$sにあり、画像の Brand Intellectual Property は%4$sによって所有され。これらのファイルを使用する前に、%5$sを遵守することに加えて、アートコンテンツを使用するために前の 2 人の作者から許可を得る必要があります。]]>
- デフォルト + KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。 + %2$s チャンネルに参加]]> + アプリのプロファイル + 既定 テンプレート カスタム プロファイル名 - グループ - ケイパビリティ - SELinux コンテキスト + 名前空間のマウント + 継承 + 共通 + 分離 モジュールのアンマウント + グループ + SELinux コンテキスト %s のアプリのプロファイルの更新をできませでした - 現在の KernelSU のバージョン「%s」は低すぎるため、マネージャーは正常に動作しません。バージョン「%s」以上に更新してください! - デフォルトでモジュールのマウントを解除する - アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 - このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 ドメイン ルール - 更新 - モジュールをダウンロード中: %s + 新しいバージョン %s が利用可能です。タップしてダウンロード! + アップデート ダウンロードを開始: %s - 最新のバージョン「%s」が利用可能です。タップしてダウンロード。 起動 - 強制停止 + 強制停止 再起動 SELinux ルールの更新に失敗しました %s + ケーパビリティ + モジュールをダウンロード中: %s + このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 + 既定でモジュールのマウントを解除 + アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 + 現在の KernelSU バージョン %d はマネージャーが適切に機能するには低すぎます。 バージョン %d 以降にアップグレードしてください! 変更履歴 - アプリプロファイルのテンプレート - アプリプロファイルのローカルおよびオンラインテンプレートを管理します。 - テンプレートの作成 - テンプレートの編集 - ID - 無効なテンプレート ID - 名前 - 説明 - 保存 - 消去 - テンプレートを表示 - 読み取り専用 - テンプレート ID はすでに存在します! - インポートとエクスポート - クリップボードからインポート + インポート成功 クリップボードからエクスポート エクスポートするローカル テンプレートが見つかりません! - インポートが成功しました - オンラインテンプレートの同期 - テンプレートの保存に失敗しました - クリップボードが空です! + テンプレート ID はすでに存在します! + クリップボードからインポート 変更ログの取得に失敗しました: %s - 更新を確認する - アプリの起動時に更新を自動で確認します。 + 名前 + 無効なテンプレート ID + オンラインテンプレートの同期 + テンプレートの作成 + 読み取り専用 + インポート/エクスポート + テンプレートの保存に失敗しました + テンプレートの編集 + ID + アプリプロファイルのテンプレート + 説明 + 保存 + アプリプロファイルのローカルおよびオンラインテンプレートを管理する。 + 消去 + クリップボードが空です! + テンプレートを表示 + アップデートを確認 + アプリを開いたときにアップデートを自動的に確認する。 root の付与に失敗しました! - アクション - 閉じる - WebView デバッグを有効化する - WebUI のデバッグに使用できます。必要な場合でのみ有効化してください。 - 直接インストール (推奨) - パッチを行うイメージを選択 - 非アクティブなスロットにインストール (OTA 後) - 再起動後、デバイスは**強制的に**、現在の非アクティブスロットから起動します。 -\nこのオプションは、OTA が完了した後にのみ使用してください。 -\n続行しますか? - 次へ - %1$s のパーティションイメージを推奨します。 + 開く + WebView デバッグ + WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。 + %1$s パーティション イメージが推奨されます KMI を選択してください - アンインストール - 一時的にアンインストールする + 次に + 非アクティブなスロットにインストール (OTA 後) + 再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。 +\nこのオプションは、OTA が完了した後にのみ使用してください。 +\n続く? + 直接インストール (推奨) + ファイルを選択してください 完全にアンインストールする ストックイメージを復元 + 一時的にアンインストールする + アンインストール KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。 - KernelSU (root およびすべてのモジュール) を完全かつ恒久的にアンインストールします。 + KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。 バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。 フラッシュ - フラッシュが成功しました - フラッシュに失敗しました + フラッシュ成功 + フラッシュ失敗 選択された LKM: %s ログを保存 + アクション 保存されたログ - - %1$s モジュールをインストールしますか? - 不明なモジュール - - モジュールの復元を確認 - この操作によりモジュールが上書きされます。続行しますか? + 並べ替え(最初に有効) + 並べ替え(アクション優先) + ⁠su コマンドを使用してアプリがルート権限を取得する機能を無効にします (既存のルート プロセスは影響を受けません)。 + %s にスーパーユーザーアクセスを許可できませんでした + su互換性を無効にする + 次のモジュールがインストールされます: %1$s 確認 - キャンセル - - バックアップが完了しました (tar.gz) - バックアップに失敗: %1$s - モジュールをバックアップ - モジュールを復元 - - モジュールは正常に復元されました、再起動が必要です - 復元に失敗: %1$s - 今すぐ再起動 - 不明なエラー - - コマンドの実行に失敗しました: %1$s - - 許可リストのバックアップが成功しました - 許可リストのバックアップに失敗: %1$s - 許可リストの復元を確認 - この操作により許可リストが上書きされます。続行しますか? - 許可リストの復元が成功しました - 許可リストの復元に失敗: %1$s - 許可リストをバックアップ - 許可リストを復元 - アプリの背景を変更 - 背景にする画像を選択してください - ナビゲーションバーの透過 - Android バージョン - デバイスモデル - 「%s」にスーパーユーザー権限を付与することはできません - su の互換性を無効化する - su コマンドを使用してアプリが root 権限を取得する動作を一時的に無効化します (既存の root プロセスは影響を受けません)。 - %1$d 個のモジュールをインストールしてもよろしいですか?\n\n%2$s - その他の設定 - SELinux - 有効 - 無効 - シンプルモード - ON にすると不要なカードを非表示にします。 - カーネル バージョンを非表示 - カーネル バージョンを非表示にします。 - その他の情報を非表示 - ナビゲーションバーページでスーパーユーザー、モジュール、KPM モジュールの数のドットを非表示にします。 - SuSFS ステータスを非表示 - ホームページ上の SuSFS ステータス情報を非表示にします。 - Zygisk のステータスを非表示 - ホームページ上の Zygisk 実装情報を非表示にします。 - リンクカードのステータスを非表示 - ホームページ上のリンクカード情報を非表示にします。 - モジュールラベルの行を非表示 - モジュールカード内のフォルダ名とサイズのラベルを非表示にします。 - テーマ - システムに従う - ライト - ダーク - 手動でフック - ダイナミックカラー - システムテーマのダイナミックカラーを使用します。 - テーマカラーを選択 - ブルー - グリーン - パープル - オレンジ - ピンク - グレー - イエロー - AnyKernel3 をインストール - AnyKernel3 カーネルファイルをフラッシュします - root 権限が必要です - スクラブが完了しました - すぐに再起動しますか? - はい - いいえ - 再起動に失敗しました - KPM - カーネルモジュールは現在インストールされていません - バージョン - 作者 - アンインストール - アンインストールに失敗しました - アンインストールに失敗しました - KPM モジュールの読み込みに成功しました - KPM モジュールの読み込みに失敗しました - パラメータ - 実行 - KPM バージョン - 閉じる - 以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました - SukiSU Ultra の今後にご期待ください - 成功 - 失敗 - SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献に感謝しています! - 非対応 - 対応 - カーネルはパッチされていません - カーネルは未設定です - カスタム設定 - KPM をインストール - 読み込む - 埋め込む - 選択してください: %1\$s モジュールのインストールモード \n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムで恒久的にインストールします - モジュールファイルが存在するか確認できません - テーマカラー - ファイルの種類が間違っています!.kpm ファイルを選択してください。 - アンインストール - 次の KPM がアンインストールされます: %s - 2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します。 - 再プロビジョニング - - フラッシュが完了しました - - 準備中… - ファイルを削除中… - ファイルをコピー中… - フラッシュツールを展開中… - フラッシュスクリプトをパッチ中… - カーネルをフラッシュ中… - フラッシュが完了しました - - フラッシュ先のスロットを選択 - フラッシュする boot のターゲットスロットを選択 - スロット A - スロット B - 選択したスロット: %1$s - オリジナルのスロットを取得 - 指定するスロットを設定 - デフォルトのスロットに復元 - 現在のシステムデフォルトスロット: %1$s - - コピーに失敗しました - 不明なエラー - フラッシュに失敗しました - - LKM の修復またはインストール - AnyKernel3 をフラッシュ - カーネル バージョン: %1$s - パッチ適用ツールの使用: %1$s - 設定 - アプリの設定 - ツール - - アプリがありません - SELinux 有効 - SELinux 無効 - SELinux ステータスの変更に失敗しました - 高度な設定 - ツールバーをカスタマイズ - 戻る - 背景の設定が成功しました - カスタム背景を削除しました - 代替アイコン - ランチャーアイコンを KernelSU のアイコンに変更します。 - アイコンを変更しました - - KPM 機能を非表示 - ホームとボトムバーから KPM の情報と機能を非表示にします。 - - WebUI で使用するエンジン - 自動選択 - WebUI X の使用を強制する - KSU WebUI の使用を強制する - WebUI に Eruda をインジェクトする - デバッグを容易にするために WebUI X にデバッグコンソールを挿入します。Web デバッグが ON になっている必要があります。 - - DPI の変更を適用 - このアプリのみで画面表示密度を調整します。 - - - - 特大 - カスタマイズ - DPI の設定を適用する - DPI の変更を確認 - アプリの DPI を %1$d から %2$d に変更してもよろしいですか? - 変更した DPI 設定を適用するにはアプリを再起動する必要がありますが、システムステータスバーや他のアプリには影響しません - DPI は %1$d に変更されました。アプリの再起動後に適用されます。 - - アプリの言語 - システムに従う - カードの暗さを調整 - - エラーコード - ログを確認してください - モジュールをインストール中: %1$d/%2$d - %d モジュールのインストールに失敗しました - モデルのダウンロードに失敗しました - カーネルをフラッシュ中 - - すべて - Root - カスタム - デフォルト - - 名前の昇順 - 名前の降順 - インストール日時 (新しい) - インストール日時 (古い) - サイズの降順 - サイズの昇順 - 使用頻度 - - このカテゴリーにアプリはありません - - 権限の認証 - 認証 - モジュールのマウントを解除 - アンインストールするモジュールのマウントを無効化します。 - メニューを展開 - メニューを収納 - 上詰め - 画面下 - 選択中 - オプション - - メニューのオプション - 並べ替え - アプリタイプを選択 - - SuSFS の構成 - 構成の説明 - この機能を使用すると SuSFS の uname の値とビルド日時の偽装をカスタマイズできます。設定する値を入力後に「適用」をタップで有効になります。 - uname の値 - カスタム uname の値を入力してください - ビルド日時を偽装 - 偽装するビルド日時を入力してください - 現在の値: %s - 現在のビルド日時: %s - デフォルトにリセット - 適用 - - リセットを確認 - - ksu_susfs ファイルが見つかりません - SuSFS コマンドの実行に失敗しました - SuSFS コマンドの実行エラー: %s - SuSFS uname とビルド日時が正常に設定されました: %s - %s - - SuSFS の構成 - - 自動起動 - システムの起動時に自動で uname の構成を適用する - 有効化するには uname を構成するかパスを追加する必要があります - 自動起動の有効化に失敗しました - 自動起動の無効化に失敗しました - 自動起動の構成エラー: %s - 自動起動に利用可能な構成がありません - - 基本設定 - SUS のパス - SUS マウント - アンマウントを試す - パスの設定 - 有効な機能のステータス - - SUS パスを追加 - SUS マウントを追加 - アンマウントを試すを追加 - SUS パスが正常に追加されました - パスが見つかりません - パス - マウントのパス - 例: /system/addon.d - SUS パスが未構成です - SUS マウントが未構成です - アンマウントを試すが未構成です - - アンマウントモード - 通常のアンマウント (0) - アンマウントを分離 (1) - 通常 - 分離 - モード: %1$s (%2$s) - 追加されたパスのアンマウントに成功しました: %s - アンマウントのパスの保存に成功しました: %s - - - SUS パスをリセット - すべての SUS パスの構成が消去されます。続行してもよろしいですか? - SUS マウントをリセット - すべての SUS マウントの構成が消去されます。続行してもよろしいですか? - リセットしてアンマウントを試す - すべてのアンマウント構成がリセットされます。続行してもよろしいですか? - パスの設定をリセット - - Android データパス - SD カードのパス - Android データパスを設定 - SD カードのパスを設定 - - SuSFS で有効な機能のステータスを表示します。 - 機能のステータス情報が見つかりません - 有効 - 無効 - - SUS パスの対応 - SUS マウントの対応 - アンマウントを試すの対応 - uname 偽装の対応 - Cmdline/Bootconfig を偽装 - オープンリダイレクトの対応 - ログの対応 - 自動でデフォルトのマウント - 自動でバインドマウント - 自動でバインドマウントのアンマウントを試す - KSU SUSFS シンボルを非表示 - SUS Kstat の対応 - SUS SU モード切り替え機能 - - 構成可能な SuSFS の機能 - SuSFS のログ取得を有効化 - SuSFS のログ取得を有効化または無効化します。 - SuSFS ログ取得の構成 - SuSFS のログ取得を有効化中 - SuSFS のログ取得を無効化 - 更新用の JSON - 更新用 JSON の URL をクリップボードにコピーしました - - モジュール情報の詳細を表示 - 更新用 JSON の URL など追加の情報を表示します。 - 実行先 - 現在の実行先: %s - サービス - Post-FS-Data - システムサービスの開始後に実行 - ファイルシステムのマウント後にシステムが完全に起動する前に実行をすることで、ブートループが発生する可能性があります。 - スロット情報 - 現在のブートスロット情報の表示と値のコピーをします。 - 現在のアクティブスロット: %s - Uname: %s - ビルド日時: %s - 現在 - Uname を使用する - ビルド日時を使用する - スロット情報を取得できません - - SuSFS 自動起動モジュールが有効、モジュールのパス: %s - SuSFS 自動起動モジュールが無効 - - Kstat の構成 - Kstat の静的構成を追加しました: %1$s - Kstat の構成を削除しました: %1$s - Kstat パスを追加しました: %1$s - Kstat パスを削除しました: %1$s - Kstat が更新されました: %1$s - Kstat のフルクローンが更新されました: %1$s - Kstat 静的構成を追加 - ファイルまたはディレクトリのパス - ヒント: オリジナルの値を使用するには「default」を使用します - Kstat のパスを追加 - 追加 - Kstat の構成をリセット - すべての Kstat の構成を消去しますか?この操作は元に戻せません。 - Kstat の構成の説明 - • add_sus_kstat_statically: ファイル、ディレクトリの静的な状態情報 - • add_sus_kstat: バインドマウント前にパスを追加して元の状態情報を保存します - • update_sus_kstat: ターゲットとなる ino を更新、サイズとブロックは変更しません - • update_sus_kstat_full_clone: ino のみ更新、他の値はそのままにします - Kstat の静的構成 - Kstat パスの管理 - Kstat の構成が未設定です。上のボタンをタップで追加します。 - - SUS マウントの非表示制御 - プロセスの SUS マウントを非表示する動作を制御します。 - すべてのプロセスで SUS マウントを非表示 - 有効化すると SUS マウントは KSU プロセスを含むすべてのプロセスから非表示になります。 - 無効化すると SUS マウントは非 KSU プロセスからのみ非表示になり、KSU プロセスはマウントを見ることができます。 - すべてのプロセスで SUS マウントの非表示を有効化しました - すべてのプロセスで SUS マウントの非表示を無効化しました - 画面のロック解除後または service.sh または boot-completed.sh の段階で無効に設定することを推奨します。これにより、KSU プロセスによってマウントされたマウントに依存する一部の root 化されたアプリの問題が解決されるはずです。 - 現在の設定: %s - すべてのプロセスを非表示 - 非 KSU プロセスのみ非表示 - 簡潔モードなカーネル バージョン - SukiSU のカーネル バージョンによって表示されるクリーンモードを有効または無効します。 - Android データパスが設定されました: %s - SD カードのパスは次のように設定済みです: %s - パスの設定は完全に成功しない可能性がありますが、SUS パスは引き続き追加されます。 - - バックアップ - SuSFS のすべての設定のバックアップを作成します。バックアップファイルは「すべての設定、パス、構成」が含まれます。 - バックアップを作成 - バックアップの作成に成功しました: %s - バックアップの作成に失敗しました: %s - バックアップファイルが見つかりません - 無効なバックアップファイル形式 - バックアップのバージョンが一致しませんが、復元を試みます。 - 復元 - SuSFS の構成をバックアップファイルから復元します。これにより、現在の設定がすべて上書きされます。 - バックアップファイルを選択 - デバイス: %s から「%s」に作成されたバックアップから構成が正常に復元されました。 - 復元に失敗しました: %s - 復元を確認 - これにより現在の SuSFS 構成がすべて上書きされます。続行してもよろしいですか? - 復元 - バックアップ日時: %s - デバイス: %s - バージョン: %s - ロック状態 - late_start サービスモードでブートローダーのロック状態属性を上書きする - 残骸をクリーンアップ - 様々なモジュールや残骸となったツールのファイルとディレクトリをクリーンアップします (誤って削除すると損失や起動の失敗に繋がる可能性があるため、注意して使用してください) - SUS のパスを編集 - SUS マウントを編集 - アンマウントを試すを編集 - Kstat 静的構成を編集 - Kstat のパスを編集 - 保存 - 編集 - 消去 - 更新 - Kstat の構成を更新 - Kstat のパスを更新 - フルクローンの SuSFS を更新 - Zygote 分離サービスをアンマウント - このオプションを有効化すると、システムの起動時に Zygote 分離サービスのマウントポイントがアンマウントされます。 - Zygote 分離サービスのアンマウントが有効です - Zygote 分離サービスのアンマウントが無効です - アプリのパス - その他のパス - その他 - アプリ - 追加のアプリパス - アプリを検索 - %1$d 個のアプリを選択済み - %1$d 個のアプリを追加済み - すべてのアプリが追加されました - 動的な署名の構成 - 有効 (サイズ: %s) - 無効 - 動的な署名を有効化 - 署名のサイズ - 署名のハッシュ - ハッシュは 64 桁の 16 進数の文字列でなければなりません。 - 動的な署名の構成が正常に設定されました - 動的な署名の構成の設定に失敗しました - 無効な署名の構成 - 動的な署名が無効です - 動的な署名の消去に失敗しました - 動的 - 署名 %1$d - 不明 - 有効なマネージャー - 有効なマネージャーがありません - SukiSU - Zygisk を実装 - - SUS ループパス - SUS ループパスを追加 - SUS ループパスを編集 - SUS ループパスが正常に追加されました: %1$s - SUS ループパスが削除されました: %1$s - SUS ループパスが更新されました: %1$s -> %2$s - SUS ループパスが構成されていません - ループパスをリセット - すべての SUS ループパスを消去してもよろしいですか?この操作は元に戻せません。 - ループパス - /data/example/path - 注意: ループパス経由で追加できるのは「/storage/」と「/sdcard/」内にないパスのみです。 - エラー: ループパスは「/storage/」または「/sdcard/」のディレクトリ内に配置できません。 - ループパス - ループパスを追加 - - ループパスの構成 - ループパスは、非 root ユーザーアプリまたは独立したサービスの起動ごとに SUS_PATH として再設定されます。これにより、追加されたパスの inode ステータスがリセットされたり、カーネル内で inode が再生成される問題に対処できます。 - AVC ログの偽装 - AVC ログの偽装が有効化されました - AVC ログの偽装が無効化されました - 無効: カーネルの AVC ログに表示される「su」の SUS T コンテキストの偽装を無効化します。\n -有効: カーネルの AVC ログに表示される「kernel」を使用して「su」の SUS T コンテキストを偽装する機能を有効化します。 - 重要な注意事項:\n -- カーネルはデフォルトで「0」に設定されています。\n -- これを有効化すると、開発者が何らかの権限や SELinux の問題をデバッグするときに原因を特定するのが難しくなる場合があるため、デバッグ時はこれを無効化することをお勧めします。 - - 検証済み - モジュールの署名が検証されました - 署名の検証 - モジュールのインストール時に署名の検証を強制します。(ARM アーキテクチャのみ) - 不明な発行元 - 署名されていないモジュールは不完全な可能性があります。デバイスを保護するため、このモジュールのインストールをブロックしました。 - 署名されていないモジュールは不完全な可能性があります。不明な発行元のモジュールをこのデバイスにインストールすることを許可しますか? - フックタイプ + モジュールの更新を確認する + ローカルLKMファイルを使用する + .koファイルのみサポートされています + カーネルのアンマウントを無効にする + KernelSU によって制御されるカーネルレベルのアンマウント動作を無効にします。 + 強化されたセキュリティを有効にする + より厳格なセキュリティ ポリシーを有効にします。 + デフォルト + 一時的に有効にする + 永続的に有効にする + 処理… + 下に引いて更新 + リリースしてリフレッシュ + 爽やか… + 更新に成功しました diff --git a/manager/app/src/main/res/values-km/strings.xml b/manager/app/src/main/res/values-km/strings.xml new file mode 100644 index 00000000..f7204411 --- /dev/null +++ b/manager/app/src/main/res/values-km/strings.xml @@ -0,0 +1,6 @@ + + + ទំព័រដើម + មិនទាន់បានដំឡើង + ចុចដើម្បីដំឡើង + diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml index e60127c6..51044c6e 100644 --- a/manager/app/src/main/res/values-kn/strings.xml +++ b/manager/app/src/main/res/values-kn/strings.xml @@ -1,362 +1,70 @@ - ಮನೆ - Not installed - Click to install - ಕೆಲಸ ಮಾಡುತ್ತಿದೆ - ವರ್ಷನ್: %s - ಬೆಂಬಲಿತವಾಗಿಲ್ಲ - KernelSU ಈಗ GKI ಕರ್ನಲ್‌ಗಳನ್ನು ಮಾತ್ರ ಬೆಂಬಲಿಸುತ್ತದೆ - ಕರ್ನಲ್ - SuSFS Version - ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್ - SELinux ಸ್ಥಿತಿ - Disabled - Enforcing - Permissive + ಪರಿಣಾಮ ಬೀರಲು ರೀಬೂಟ್ ಮಾಡಿ + KernelSU ಅನ್ನು ಹೇಗೆ ಸ್ಥಾಪಿಸಬೇಕು ಮತ್ತು ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ ತಿಳಿಯದ - ಸೂಪರ್ಯೂಸರ್ + ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ತೋರಿಸಿ + %s ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ + Umount ಮಾಡ್ಯೂಲ್‌ಗಳು + ಲಾಗ್ ಕಳುಹಿಸಿ + ನಮ್ಮನ್ನು ಬೆಂಬಲಿಸಿ + ಪಿತ್ರಾರ್ಜಿತ + ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಏಕೆಂದರೆ ಇದು ಮ್ಯಾಜಿಸ್ಕ್‌ನೊಂದಿಗೆ ಸಂಘರ್ಷವಾಗಿದೆ! + ಚೇಂಜ್ಲಾಗ್ + Permissive + ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ + ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. + ವೈಯಕ್ತಿಕ ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s + ಫೋರ್ಸ್ ಸ್ಟಾಪ್ + EDL ಗೆ ರೀಬೂಟ್ + ಸಾಮರ್ಥ್ಯಗಳು + ಸೂಪರ್‌ಯೂಸರ್‌ಗಳು: %d + ಡೌನ್‌ಲೋಡ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ: %s + ಜಾಗತಿಕ + ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. + ಮಾಡ್ಯೂಲ್‌ಗಳು: %d + SELinux ಸಂದರ್ಭ + ಡೀಫಾಲ್ಟ್ + ಲಾಂಚ್ + ಸುರಕ್ಷಿತ ಮೋಡ್ + ಪ್ರಸ್ತುತ KernelSU ಆವೃತ್ತಿ %d ಮ್ಯಾನೇಜರ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸಲು ತುಂಬಾ ಕಡಿಮೆಯಾಗಿದೆ. ದಯವಿಟ್ಟು ಆವೃತ್ತಿ %d ಅಥವಾ ಹೆಚ್ಚಿನದಕ್ಕೆ ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಿ! + ಸಾಫ್ಟ್ ರೀಬೂಟ್ + ಪ್ರೊಫೈಲ್ ಹೆಸರು + KernelSU ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ ಮತ್ತು ಯಾವಾಗಲೂ ಇರುತ್ತದೆ. ಆದಾಗ್ಯೂ ನೀವು ದೇಣಿಗೆ ನೀಡುವ ಮೂಲಕ ನೀವು ಕಾಳಜಿ ವಹಿಸುತ್ತೀರಿ ಎಂದು ನಮಗೆ ತೋರಿಸಬಹುದು. + ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ + ಮೌಂಟ್ ನೇಮ್‌ಸ್ಪೇಸ್ + ನಿಯಮಗಳು + ಗುಂಪುಗಳು + ಓವರ್‌ಲೇಫ್‌ಗಳು ಲಭ್ಯವಿಲ್ಲ, ಮಾಡ್ಯೂಲ್ ಕಾರ್ಯನಿರ್ವಹಿಸುವುದಿಲ್ಲ! + ಮಾಡ್ಯೂಲ್ + ಲೇಖಕ + ಬಗ್ಗೆ + ವರ್ಷನ್: %d + ರೀಬೂಟ್ + KernelSU ಈಗ GKI ಕರ್ನಲ್‌ಗಳನ್ನು ಮಾತ್ರ ಬೆಂಬಲಿಸುತ್ತದೆ + SELinux ಸ್ಥಿತಿ + ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಮರೆಮಾಡಿ + ವರ್ಷನ್ + ಬೆಂಬಲಿತವಾಗಿಲ್ಲ + ಡೊಮೇನ್ + ಮನೆ + ಕಸ್ಟಮ್ + ಟೆಂಪ್ಲೇಟ್ + ರಿಫ್ರೆಶ್ + ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ: %s + KernelSU ಕಲಿಯಿರಿ + %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ\? + ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ: %s + ಸೂಪರ್ಯೂಸರ್ + ಕೆಲಸ ಮಾಡುತ್ತಿದೆ ಮಾಡ್ಯೂಲ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ - ಮಾಡ್ಯೂಲ್ - Sort (Action first) - Sort (Enabled first) - ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ - Install - Install - ರೀಬೂಟ್ - Settings - ಸಾಫ್ಟ್ ರೀಬೂಟ್ - Reboot to Recovery - Reboot to Bootloader - Reboot to Download - EDL ಗೆ ರೀಬೂಟ್ - ಬಗ್ಗೆ - %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ\? - %s ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ - ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ: %s - ವರ್ಷನ್ - ಲೇಖಕ - ರಿಫ್ರೆಶ್ - ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ತೋರಿಸಿ - ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಮರೆಮಾಡಿ - ಲಾಗ್ ಕಳುಹಿಸಿ - ಸುರಕ್ಷಿತ ಮೋಡ್ - ಪರಿಣಾಮ ಬೀರಲು ರೀಬೂಟ್ ಮಾಡಿ - ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಏಕೆಂದರೆ ಇದು ಮ್ಯಾಜಿಸ್ಕ್‌ನೊಂದಿಗೆ ಸಂಘರ್ಷವಾಗಿದೆ! - KernelSU ಕಲಿಯಿರಿ - https://kernelsu.org/guide/what-is-kernelsu.html - KernelSU ಅನ್ನು ಹೇಗೆ ಸ್ಥಾಪಿಸಬೇಕು ಮತ್ತು ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ - ನಮ್ಮನ್ನು ಬೆಂಬಲಿಸಿ - KernelSU ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ ಮತ್ತು ಯಾವಾಗಲೂ ಇರುತ್ತದೆ. ಆದಾಗ್ಯೂ ನೀವು ದೇಣಿಗೆ ನೀಡುವ ಮೂಲಕ ನೀವು ಕಾಳಜಿ ವಹಿಸುತ್ತೀರಿ ಎಂದು ನಮಗೆ ತೋರಿಸಬಹುದು. - Join our %2$s channel]]> - ಡೀಫಾಲ್ಟ್ - ಟೆಂಪ್ಲೇಟ್ - ಕಸ್ಟಮ್ - ಪ್ರೊಫೈಲ್ ಹೆಸರು - ಗುಂಪುಗಳು - ಸಾಮರ್ಥ್ಯಗಳು - SELinux ಸಂದರ್ಭ - Umount ಮಾಡ್ಯೂಲ್‌ಗಳು + ಕರ್ನಲ್ %s ಗಾಗಿ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ - ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. - ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. - ಡೊಮೇನ್ - ನಿಯಮಗಳು - Update - ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ: %s - ಡೌನ್‌ಲೋಡ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ: %s + https://kernelsu.org/guide/what-is-kernelsu.html + %1$s ನಲ್ಲಿ ಮೂಲ ಕೋಡ್ ಅನ್ನು ವೀಕ್ಷಿಸಿ
ನಮ್ಮ %2$s ಚಾನಲ್‌ಗೆ ಸೇರಿ
+ ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್ ಹೊಸ ಆವೃತ್ತಿ: %s ಲಭ್ಯವಿದೆ, ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ - ಲಾಂಚ್ - ಫೋರ್ಸ್ ಸ್ಟಾಪ್ - Restart - Failed to update SELinux rules for %s - ಚೇಂಜ್ಲಾಗ್ - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 8e6ee7de..c96772c5 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -3,16 +3,18 @@ 설치되지 않음 이 곳을 눌러 설치하기 - 정상 작동 중 - 버전: %s + 작동 중 + 버전: %d + 슈퍼유저: %d개 + 모듈: %d개 지원되지 않음 - KernelSU는 현재 GKI 커널만 지원합니다 - 커널 - SuSFS Version + KernelSU는 현재 GKI 커널만 지원합니다. + 커널 버전 매니저 버전 + 핑거프린트 SELinux 상태 비활성화됨 - 적용 + 강제 허용 알 수 없음 슈퍼유저 @@ -20,15 +22,13 @@ 모듈 비활성화 실패: %s 설치된 모듈 없음 모듈 - 정렬 (동작이 있는 것 우선) - 정렬 (활성화됨 우선) - 삭제 + 제거 설치 설치 다시 시작 설정 빠른 다시 시작 - 복구 모드로 다시 시작 + 리커버리로 다시 시작 부트로더로 다시 시작 다운로드 모드로 다시 시작 EDL 모드로 다시 시작 @@ -38,325 +38,117 @@ 모듈 삭제 실패: %s 버전 제작자 + 커널에서 OverlayFS가 비활성화되어 모듈을 사용할 수 없습니다! 새로고침 - 시스템 앱 보이기 + 시스템 앱 표시 시스템 앱 숨기기 로그 보내기 안전 모드 다시 시작하여 변경 사항 적용 Magisk와 충돌로 모듈을 사용할 수 없습니다! KernelSU 알아보기 + KernelSU 설치 및 모듈 사용 방법을 확인합니다. + 지원하기 + KernelSU는 현재와 미래에도 항상 무료이며 오픈 소스입니다. 기부를 통해 여러분의 관심과 지원을 표현해 주세요. + %2$s 채널 참가하기]]> https://kernelsu.org/guide/what-is-kernelsu.html - KernelSU 설치 방법과 모듈 사용 방법을 확인합니다 - 지원이 필요합니다 - KernelSU는 지금도, 앞으로도 항상 무료이며 오픈 소스로 유지됩니다. 기부를 통해 여러분의 관심을 보여주세요. - Join our %2$s channel]]> - 기본값 - 템플릿 - 사용자 지정 - 프로필 이름 - 사용자 그룹 - 권한 - SELinux 컨텍스트 - 모듈 사용 해제 - %s에 대한 앱 프로필 업데이트 실패 - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - 기본값으로 모듈 사용 해제 - 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. - 이 옵션이 활성화되면, KernelSU는 이 앱에 대한 모듈의 모든 수정사항을 복구합니다. - 도메인 - 규칙 - 업데이트 - 모듈 받는 중: %s - 다운로드 시작: %s - 새 버전: %s이 사용 가능합니다, 여기를 눌러 업그레이드하세요. - 실행 - 강제 중지 + 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 지정합니다. 활성화하면 개별 프로필이 설정되지 않은 앱에는 시스템에 대한 모듈의 모든 수정 사항이 제외됩니다. 다시 시작 - 다음 앱에 대한 SELinux 규칙 업데이트 실패: %s + 규칙 + 새 %s 버전을 사용할 수 있습니다, 여기를 눌러 업그레이드하세요! + 다운로드 시작: %s + 강제 중지 + 기본값 + 사용자 지정 + 템플릿 + 프로필 이름 + 마운트할 네임스페이스 + 상속 + 전역 + 개별 + 사용자 그룹 + 모듈 마운트 해제 + SELinux 컨텍스트 + 권한 + %s에 대한 앱 프로필 업데이트 실패 + 기본값으로 모듈 마운트 해제 + 이 옵션이 활성화되면 KernelSU는 이 앱에 대한 모듈의 모든 수정 사항을 복구합니다. + 업데이트 + 모듈 다운로드 중: %s + 도메인 + 실행 + %s 앱에 대한 SELinux 규칙 업데이트 실패 + 로그 저장 업데이트 내역 - 앱 프로필 템플레이트 - 앱 프로필의 로컬 및 온라인 템플레이트 관리 - 템플레이트 생성 - 템플레이트 편집 + WebUI 디버깅에 사용 가능, 필요한 경우에만 활성화하세요. + 플래싱 중 + 선택된 LKM: %s + %1$s 파티션 이미지가 권장됩니다 + KMI 선택 + 다음 + 완전하고 영구적으로 KernelSU(루트 권한 및 모든 모듈)를 제거합니다. + WebView 디버깅 + 현재 KernelSU %d 버전은 매니저가 정상 작동하기에 너무 낮습니다. %d 버전 이상으로 업그레이드하세요! + 액션 + 임시 제거 + 업데이트 내역 불러오기 실패: %s + 열기 + 재부팅 후 기기가 **강제로** 비활성 슬롯으로 부팅합니다!\nOTA가 완료된 후에만 이 옵션을 사용하세요.\n계속할까요? + 플래싱 성공 + 플래싱 실패 + 제거 + 영구 제거 + 임시적으로 KernelSU를 제거하고, 다음 재부팅 이후 기존 상태로 복구합니다. + 앱 프로필 템플릿 + 로컬 및 온라인의 앱 프로필 템플릿을 관리합니다. ID - 올바르지 않은 템플레이트 id + 잘못된 템플릿 ID 이름 설명 저장 삭제 - 템플레이트 보기 읽기 전용 - 템플레이트 ID가 이미 존재합니다! + 템플릿 ID가 이미 존재합니다! 불러오기/내보내기 클립보드에서 불러오기 클립보드로 내보내기 - 내보낼 로컬 템플레이트가 없습니다! 불러오기 성공 - 온라인 템플레이트 동기화 - 템플레이트 저장 실패 - 클립보드가 비었습니다! - 업데이트 내역 가져오기 실패: %s - 업데이트 확인 - 앱 실행시 자동으로 업데이트 확인 - 루트 부여 실패! - 동작 - Close - WebView 디버깅 활성화 - WebUI 디버깅에 사용 가능, 필요할 때만 활성화해주세요. - 직접 설치 (권장) + 온라인 템플릿 동기화 + 템플릿 저장 실패 + 클립보드가 비어 있습니다! + 루트 권한 부여 실패! + 템플릿 생성 + 템플릿 편집 + 템플릿 보기 + 내보낼 로컬 템플릿을 찾을 수 없습니다! 파일 선택 + 직접 설치 (권장) 비활성 슬롯에 설치 (OTA 이후) - 재부팅 후 기기는 **강제로** 비활성 슬롯으로 부팅합니다!\nOTA를 진행한 후에만 이 옵션을 사용하세요.\n진행할까요? - 다음 - %1$s 파티션 이미지 권장됨 - KMI 선택 - 삭제 - 임시적 삭제 - 영구적 삭제 순정 이미지 복구 - 임시적으로 KernelSU를 삭제하고, 다음 재부팅에 원래대로 복구합니다. - 완전히, 그리고 영구히 KernelSU (루트 및 모든 모듈)를 삭제합니다. - 순정 이미지 복구 (백업이 존재한다면), OTA 전에 사용합니다; KernelSU를 삭제해야 한다면, \"영구적 삭제\"를 사용해 주세요. - 플래시 중 - 플래시 성공 - 플래시 실패 - 선택된 LKM: %s - 로그 저장 + 순정 이미지 복구 (백업 존재 시), OTA 이전에 사용합니다. KernelSU를 제거해야 한다면, \"영구 삭제\"를 사용하세요. + 업데이트 확인 + 앱 실행 시 자동으로 업데이트를 확인합니다. 로그 저장됨 - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + 정렬 (활성화 기준) + 정렬 (액션 기준) + 다음 모듈이 설치됩니다: %1$s + 확인 + %s에 슈퍼유저 권한을 부여할 수 없습니다 + 모듈 업데이트 확인 + 로컬 LKM 파일 사용 + .ko 파일만 지원됩니다 + su 호환 비활성화 + su 명령어로 아무 앱이 루트 권한을 획득하는 것을 비활성화합니다 (이미 존재하는 루트 프로세스는 영향을 받지 않음). + 커널 마운트 해제 비활성화 + KernelSU에서 제어하는 커널 수준 마운트 해제 동작을 비활성화합니다. + 향상된 보안 활성화 + 더욱 엄격한 보안 규칙을 활성화합니다. + 기본값 + 임시 활성화 + 항상 활성화 + 처리 중… + 당겨서 새로 고침 + 놓아서 새로 고침 + 새로 고침 중… + 새로 고침 성공 diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml index 4206c33f..3699e4a8 100644 --- a/manager/app/src/main/res/values-lt/strings.xml +++ b/manager/app/src/main/res/values-lt/strings.xml @@ -1,68 +1,50 @@ - Namai - Neįdiegta - Spustelėkite norėdami įdiegti - Veikia - Versija: %s - Nepalaikoma - KernelSU dabar palaiko tik GKI branduolius - Branduolys - SuSFS Version - Tvarkyklės versija - SELinux statusas + Pirštų atspaudas Išjungta Priverstinas - Leistinas Nežinomas Supernaudotojai Nepavyko įjungti modulio: %s Nepavyko išjungti modulio: %s + Leistinas Nėra įdiegtų modulių Moduliai - Sort (Action first) - Sort (Enabled first) - Išdiegti - Įdiegti - Įdiegti - Paleisti iš naujo - Parametrai Perkrovimas neišjungus Perkrauti į atkūrimo rėžimą Perkrauti į įkrovos tvarkyklę Perkrauti į atsisiuntimo rėžimą - Perkrauti į EDL Apie - Ar tikrai norite išdiegti modulį %s\? - %s išdiegtas Nepavyko išdiegti: %s + %s išdiegtas Versija Autorius - Atšviežinti + overlayfs nepasiekiamas, modulis negali veikti! Rodyti sistemos programas Slėpti sistemos programas Siųsti žurnalą + Paleisti iš naujo + Atšviežinti Saugus rėžimas Paleiskite iš naujo, kad įsigaliotų Moduliai yra išjungti, nes jie konfliktuoja su Magisk\'s! - Sužinokite apie KernelSU https://kernelsu.org/guide/what-is-kernelsu.html + Sužinokite apie KernelSU Sužinokite, kaip įdiegti KernelSU ir naudoti modulius - Paremkite mus - KernelSU yra ir visada bus nemokamas ir atvirojo kodo. Tačiau galite parodyti, kad jums rūpi, paaukodami mums. - Join our %2$s channel]]> + Peržiūrėkite šaltinio kodą %1$s
Prisijunkite prie mūsų %2$s kanalo
Numatytas Šablonas Pasirinktinis Profilio pavadinimas + Prijungti vardų erdvę + Paveldėtas + Globalus + Individualus Grupės Galimybės SELinux kontekstas Atjungti modulius - Nepavyko atnaujinti programos profilio %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Atjungti modulius pagal numatytuosius parametrus - Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai. Domenas Taisyklės @@ -71,292 +53,32 @@ Pradedamas atsisiuntimas: %s Nauja versija: %s pasiekiama, spustelėkite norėdami atsinaujinti Paleisti - Priversti sustoti + Priversti sustoti Perkrauti Nepavyko atnaujinti SELinux taisyklių: %s + Namai + Neįdiegta + KernelSU dabar palaiko tik GKI branduolius + Spustelėkite norėdami įdiegti + Veikia + Supernaudotojai: %d + Versija: %d + Nepalaikoma + Moduliai: %d + Tvarkyklės versija + Branduolys + SELinux statusas + Išdiegti + Įdiegti + Įdiegti + Parametrai + Perkrauti į EDL + Ar tikrai norite išdiegti modulį %s\? + Paremkite mus + KernelSU yra ir visada bus nemokamas ir atvirojo kodo. Tačiau galite parodyti, kad jums rūpi, paaukodami mums. + Nepavyko atnaujinti programos profilio %s + Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. Keitimų žurnalas - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s + Ši KernelSU versija %d yra per žema, kad šis vadybininkas galėtų tinkamai funkcionuoti. Prašome atsinaujinti į versiją %d ar aukščiau! Saglabāt Žurnālus - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml index 92b82876..e34a3eee 100644 --- a/manager/app/src/main/res/values-lv/strings.xml +++ b/manager/app/src/main/res/values-lv/strings.xml @@ -1,69 +1,69 @@ + Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. + Neizdevās atjaunināt SELinux noteikumus: %s + Pārvaldiet vietējo un tiešsaistes lietotņu profila veidni + Nederīgs veidnes id + veidnes id jau pastāv! + Eksportēt starpliktuvē + Importēt no starpliktuves + Importēts veiksmīgi + Sinhronizēt tiešsaistes veidnes Sākums - Nav ieinstalēts - Noklikšķiniet, lai instalētu + Nav uzstādīts + Noklikšķiniet, lai uzstādītu Darbojas - Versija: %s + Versija: %d + Superlietotāji: %d + Moduļi: %d Neatbalstīts - KernelSU atbalsta tikai GKI kodolus + KernelSU pagaidām atbalsta tikai GKI kodolus Kodols - SuSFS Version Pārvaldnieka versija + Pirkstu nospiedums SELinux statuss + Piespiests Atspējots - Izpildīšana - Visatļautība Nezināms - SuperLietotājs - Neizdevās iespējot moduli: %s + Superlietotājs Neizdevās atspējot moduli: %s - Nav instalētu moduļu + Nav uzstādīts neviens modulis Moduļi - Sort (Action first) - Sort (Enabled first) - Atinstalēt - Instalēt - Instalēt + Noņemt + Uzstādīšana Restartēt Iestatījumi Ātri restartēt - Restartēt uz Recovery - Restartēt uz Bootloaderu - Restartēt uz Download - Restartēt uz EDL - Par - Vai tiešām vēlaties atinstalēt moduli %s? - %s ir atinstalēts - Neizdevās atinstalēt: %s - Versija + Restartēt uz Sāknēšanas režīmu + Restartēt uz Atkopšanas režīmu + Restartēt uz Lejupielādes režīmu + Restartēt uz EDL režīmu + Par lietotni + %s noņemts + Neizdevās noņemt: %s Autors - Atjaunot + Atsvaidzināt Rādīt sistēmas lietotnes Slēpt sistēmas lietotnes - Ziņot žurnālu - Drošais režīms + Sūtīt žurnālus Restartējiet, lai stātos spēkā - Moduļi ir atspējoti, jo tie konfliktē ar Magisk! Uzzināt par KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Uzzināt, kā instalēt KernelSU un izmantot moduļus Atbalsti mūs - KernelSU ir un vienmēr būs bezmaksas un atvērtā koda. Tomēr jūs varat parādīt mums, ka jums rūp, veicot ziedojumu. - Join our %2$s channel]]> + Skatiet avota kodu vietnē %1$s
Pievienojies mūsu %2$s kanālam
Noklusējums Veidne Pielāgots Profila vārds - Grupas + Mount nosaukumvieta + Individuāls Iespējas SELinux konteksts Atvienot moduļus Neizdevās atjaunināt lietotnes profilu %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Pēc noklusējuma atvienot moduļus Globālā noklusējuma vērtība vienumam “Atvienot moduļus” lietotņu profilos. Ja tas ir iespējots, lietojumprogrammām, kurām nav iestatīts profils, tiks noņemtas visas sistēmas moduļu modifikācijas. - Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. Domēns Noteikumi Atjaunināt @@ -71,294 +71,65 @@ Sākt lejupielādi: %s Jaunā versija: %s ir pieejama, noklikšķiniet, lai atjauninātu Palaist - Piespiedu apstāšana + Piespiedu apstāšana Restartēt aplikāciju - Neizdevās atjaunināt SELinux noteikumus: %s Izmaiņu žurnāls Lietotnes profila veidne - Pārvaldiet vietējo un tiešsaistes lietotņu profila veidni Izveidot veidni Rediģēt veidni id - Nederīgs veidnes id Vārds Apraksts Saglabāt Dzēst Skatīt veidni tikai lasīt - veidnes id jau pastāv! Importēt/Eksportēt - Importēt no starpliktuves - Eksportēt starpliktuvē Nevar atrast vietējo eksportējamo veidni! - Importēts veiksmīgi - Sinhronizēt tiešsaistes veidnes Neizdevās saglabāt veidni Starpliktuve ir tukša! Izmaiņu žurnāla iegūšana neizdevās: %s - Pārbaudīt atjauninājumus - Automātiski pārbaudīt atjauninājumus atverot aplikāciju - Neizdevās piešķirt sakni! - Action - Close + Visatļautība + Neizdevās iespējot moduli: %s + Uzstādīt + Vai tiešām vēlaties noņemt %s moduli? + Versija + Moduļi nav pieejami, jo kodols ir atspējojis OverlayFS! + Drošais režīms + Moduļi nav pieejami dēļ konflikta ar Magisk! + KernelSU ir un vienmēr būs bezmaksas un atvērtā koda. Tomēr jūs varat parādīt mums, ka jums rūp, veicot ziedojumu. + Grupas + Globāli + Pašreizējā KernelSU versija %d ir pārāk zema, lai pārvaldnieks darbotos pareizi. Lūdzu, atjauniniet uz versiju %d vai jaunāku! Iespējot WebView atkļūdošanu - Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams. - Tiešā instalēšana (Ieteicams) + Ieteicams %1$s nodalījuma attēls + Nākamais + Mantots Izvēlieties failu Instalēt neaktīvajā slotā (pēc OTA) Pēc restartēšanas jūsu ierīce tiks **PIESPIESTI** palaista pašreizējā neaktīvajā slotā! \nIzmantojiet šo opciju tikai pēc OTA pabeigšanas \nTurpināt? - Nākamais - Ieteicams %1$s nodalījuma attēls - Izvēlieties KMI + Tiešā instalēšana (Ieteicams) Atinstalēt Pagaidu atinstalēšana - Neatgriezeniski atinstalēt Atjaunot oriģinālo attēlu Īslaicīgi atinstalēt KernelSU, pēc nākamās restartēšanas atjaunot sākotnējo stāvokli. KernelSU (saknes un visu moduļu) pilnīga atinstalēšana. Atjaunojot rūpnīcas attēlu (ja ir dublējums), ko parasti izmanto pirms OTA; ja nepieciešams atinstalēt KernelSU, lūdzu, izmantojiet \"Neatgriezeniski atinstalēt\". + Izvēlētais lkm: %s + Neizdevās piešķirt sakni! + Atvērt + Pārbaudīt atjauninājumus + Automātiski pārbaudīt atjauninājumus atverot aplikāciju + Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams. + Izvēlieties KMI + Neatgriezeniski atinstalēt Instalē Instalēts veiksmīgi Instalēšana neizdevās - Izvēlētais lkm: %s Išsaugoti Žurnalus - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Kārtot (Iespējotie augšgalā) + Apstiprināt + Tiks uzstādīti šādi moduļi : %1$s
diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml index d070c17d..15c4dba9 100644 --- a/manager/app/src/main/res/values-mr/strings.xml +++ b/manager/app/src/main/res/values-mr/strings.xml @@ -1,362 +1,82 @@ - होम इंस्टॉल केले नाही + होम इंस्टॉल साठी क्लिक करा कार्यरत - आवृत्ती: %s + आवृत्ती: %d + मॉड्यूल्स: %d + सुपरयूझर: %d असमर्थित KernelSU आता फक्त GKI कर्नलचे समर्थन करते कर्नल - SuSFS Version + फिंगरप्रिंट व्यवस्थापक आवृत्ती SELinux स्थिती अक्षम एनफोर्सिंग परमिसिव अज्ञात + स्थापित करा + कोणतेही मॉड्यूल स्थापित केलेले नाही + रीबूट करा सुपरयुझर मॉड्यूल सक्षम करण्यात अयशस्वी: %s - मॉड्यूल अक्षम करण्यात अयशस्वी: %s - कोणतेही मॉड्यूल स्थापित केलेले नाही - मॉड्यूल - Sort (Action first) - Sort (Enabled first) विस्थापित करा + मॉड्यूल अक्षम करण्यात अयशस्वी: %s + मॉड्यूल स्थापित करा - स्थापित करा - रीबूट करा सेटिंग्ज सॉफ्ट रीबूट - रिकवरी मध्ये रिबुट करा - बूटलोडरवर रीबूट करा - डाउनलोड करण्यासाठी रीबूट करा - EDL वर रीबूट करा बद्दल + EDL वर रीबूट करा तुमची खात्री आहे की तुम्ही मॉड्यूल %s विस्थापित करू इच्छिता\? - %s विस्थापित विस्थापित करण्यात अयशस्वी: %s + overlayfs उपलब्ध नाही, मॉड्यूल काम करू शकत नाही! + सिस्टम अॅप्स दाखवा + बूटलोडरवर रीबूट करा + %s विस्थापित आवृत्ती लेखक रिफ्रेश करा - सिस्टम अॅप्स दाखवा - सिस्टम अॅप्स लपवा + रिकवरी मध्ये रिबुट करा + डाउनलोड करण्यासाठी रीबूट करा लॉग पाठवा सुरक्षित मोड + सिस्टम अॅप्स लपवा प्रभावी होण्यासाठी रीबूट करा - मॉड्यूल अक्षम केले आहेत कारण ते Magisk च्या विरोधाभास आहे! KernelSU शिका https://kernelsu.org/guide/what-is-kernelsu.html + मॉड्यूल अक्षम केले आहेत कारण ते Magisk च्या विरोधाभास आहे! KernelSU कसे स्थापित करायचे आणि मॉड्यूल कसे वापरायचे ते शिका - आम्हाला पाठिंबा द्या KernelSU विनामूल्य आणि मुक्त स्रोत आहे, आणि नेहमीच असेल. तथापि, देणगी देऊन तुम्ही आम्हाला दाखवू शकता की तुमची काळजी आहे. - Join our %2$s channel]]> + आम्हाला पाठिंबा द्या + कस्टम + माउंट नेमस्पेस डीफॉल्ट साचा - कस्टम - प्रोफाइल नाव - गट + वैयक्तिक क्षमता + %1$s वर स्रोत कोड पहा
आमच्या %2$s चॅनेलमध्ये सामील व्हा
+ प्रोफाइल नाव + इनहेरीटेड + जागतिक + गट SELinux संदर्भ उमाउंट मॉड्यूल्स %s साठी अॅप प्रोफाइल अपडेट करण्यात अयशस्वी - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा अॅप प्रोफाइलमधील \"उमाउंट मॉड्यूल्स\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल. हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल. - डोमेन + यासाठी SELinux नियम अपडेट करण्यात अयशस्वी: %s नियम अपडेट करा + डोमेन मॉड्यूल डाउनलोड करत आहे: %s डाउनलोड करणे सुरू करा: %s नवीन आवृत्ती: %s उपलब्ध आहे, डाउनलोड करण्यासाठी क्लिक करा + सक्तीने थांबा लाँच करा - सक्तीने थांबा पुन्हा सुरू करा - यासाठी SELinux नियम अपडेट करण्यात अयशस्वी: %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s लॉग जतन करा - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml index 245e8b57..67215ee7 100644 --- a/manager/app/src/main/res/values-ms/strings.xml +++ b/manager/app/src/main/res/values-ms/strings.xml @@ -1,362 +1,38 @@ - Layar Utama - Tidak terpasang - Tekan untuk memasang - Berjalan - Versi: %s - Tidak Disokong - KernelSU ketika ini hanya menyokong kernel GKI - Kernel - SuSFS Version - Versi manager - Status SELinux - Lumpuhkan - Enforcing - Permisif Tidak Diketahui - Superuser + Lumpuhkan + Permisif + Reboot ke Download Modul tidak berjaya diaktifkan: %s - Gagal mematikan modul: %s - Tiada modul dipasang - Modul - Sort (Action first) - Sort (Enabled first) + Reboot ke EDL + Superusers: %d + Modul: %d + Enforcing + Cap Jari + Reboot ke Recovery + Soft Reboot Padam Pasang - Pasang - Reboot - Tetapan - Soft Reboot - Reboot ke Recovery - Reboot ke Bootloader - Reboot ke Download - Reboot ke EDL + Tekan untuk memasang + Modul Tentang + Versi: %d + Reboot + KernelSU ketika ini hanya menyokong kernel GKI + Status SELinux + Tidak Disokong + Layar Utama Apakah anda pasti ingin membuang modul %s\? - %s uninstalled - Failed to uninstall: %s - Version - Author - Refresh - Show system apps - Hide system apps - Send logs - Safe mode - Reboot to take effect - Modules are unavailable due to a conflict with Magisk! - Learn KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Learn how to install KernelSU and use modules - Support Us - KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation. - Join our %2$s channel]]> - Default - Template - Custom - Profile name - Groups - Capabilities - SELinux context - Umount modules - Failed to update App Profile for %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Umount modules by default - The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. - Enabling this option will allow KernelSU to restore any modified files by the modules for this app. - Domain - Rules - Update - Downloading module: %s - Start downloading: %s - New version %s is available, click to upgrade. - Launch - Force stop - Restart - Failed to update SELinux rules for %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s + Superuser + Tetapan + Berjalan + Gagal mematikan modul: %s + Tiada modul dipasang + Pasang + Kernel + Tidak terpasang + Reboot ke Bootloader + Versi manager Simpan Log - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - diff --git a/manager/app/src/main/res/values-my/strings.xml b/manager/app/src/main/res/values-my/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/manager/app/src/main/res/values-my/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/manager/app/src/main/res/values-nl/strings.xml b/manager/app/src/main/res/values-nl/strings.xml index 3e516f12..ad39d491 100644 --- a/manager/app/src/main/res/values-nl/strings.xml +++ b/manager/app/src/main/res/values-nl/strings.xml @@ -4,12 +4,14 @@ Niet geïnstalleerd Klik om te installeren Werkend - Versie: %s + Versie: %d + Supergebruikers: %d + Modules: %d Niet ondersteund KernelSU ondersteunt alleen GKI kernels - Kernel - SuSFS Version + Kernel version Manager versie + Fingerprint SELinux status Uitgeschakeld Afgedwongen @@ -20,8 +22,6 @@ Mislukt om module uit te schakelen: %s Geen module geïnstalleerde Module - Sorteren (actie eerst) - Sorteren (eerst ingeschakeld) Verwijderen Installeren Installeren @@ -38,6 +38,7 @@ Mislukt om te verwijderen: %s Versie Auteur + Modules zijn niet beschikbaar omdat OverlayFS door de kernel is uitgeschakeld! Vernieuwen Toon systeem apps Verberg systeem apps @@ -49,18 +50,22 @@ https://kernelsu.org/guide/what-is-kernelsu.html Leer hoe KernelSU te installeren en modules te gebruiken Ondersteun ons - KernelSU is, en zal altijd, vrij en open source zijn. Je kan altijd je appreciatie tonen met een donatie. - Join our %2$s channel]]> + KernelSU is en blijft gratis en open source. U kunt ons echter laten zien dat u ons steunt door een donatie te doen. + Vervoeg ons %2$s kanaal]]> + App profiel Standaard Sjabloon Aangepast Profiel naam + Koppel naamruimte + Overgenomen + Globaal + Individuëel Groepen Mogelijkheden SELinux context Ontkoppel modules Mislukt om App Profiel te updaten voor %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Ontkoppel standaard de modules De globale standaardwaarde voor \"Umount modules\" in App Profile. Als dit is ingeschakeld, worden alle modulewijzigingen in het systeem verwijderd voor apps waarvoor geen profiel is ingesteld. Met deze optie ingeschakeld zal KernelSU toelaten om alle gewijzigde bestanden door de modules voor deze app te herstellen. @@ -68,297 +73,85 @@ Regels Update Downloaden van module: %s - Begin met downloaden: %s - Nieuwe versie %s is beschikbaar,klik om te upgraden. + Nieuwe versie %s is beschikbaar,klik om te upgraden! Start - Forceer stop + Forceer stop Herstart - Kan SELinux-regels niet bijwerken voor: %s + Begin met downloaden: %s + Kan SELinux-regels niet bijwerken voor %s + De huidige KernelSU-versie %d is te laag voor de manager om goed te werken. Upgrade naar versie %d of hoger! wijzigings logboek - App-profiel Sjabloon - Beheer lokale en online sjabloon van app-profiel + App-profiel sjabloon Maken sjabloon Bewerkin sjabloon ID Ongeldige sjabloon ID Naam - Beschrijving Redde - Verwijderen Bekijken sjabloon + Beschrijving + Beheer lokale en online sjabloon van app-profiel. + Verwijderen Alleen lezen Sjabloon ID bestaat al! - Importeren/Exporteren - Importeren vanaf klembord - Exporteren naar klembord - Kan niet geen lokale sjabloon vinden om te exporteren! - Succesvol geïmporteerd Synchroniseer online-sjablonen Mislukt naar opslaan sjabloon Klembord is leeg! + Importeren/Exporteren + Importeren vanaf klembord Ophalen van wijzigingslogboek mislukt: %s - Controleer update - Controleer automatisch op updates bij het openen van de app - Kan geen root verlenen! - Actie - Close - Schakel WebView-foutopsporing + Exporteren naar klembord + Controleer for updates + WebView foutopsporing Kan worden gebruikt om WebUI te debuggen. Schakel dit alleen in als dat nodig is. + Kan niet geen lokale sjabloon vinden om te exporteren! + Succesvol geïmporteerd + Open + Controleer automatisch op updates bij het openen van de app. Directe installatie (aanbevolen) Selecteer een bestand - Installeren in inactief slot (na OTA) + Kan geen root verlenen! + %1$s partitie-image wordt aanbevolen + Naast Uw apparaat wordt **GEFORCEERD** om na een herstart op te starten naar het huidige inactieve slot! \nGebruik deze optie alleen nadat OTA is voltooid. \nDoorgaan? - Naast - %1$s partitie-image wordt aanbevolen + Installeren in inactief slot (na OTA) KMI selecteren Desinstalleren Tijdelijk verwijderen Permanent verwijderen Herstel stockafbeelding Verwijder KernelSU tijdelijk en herstel het naar de oorspronkelijke staat na de volgende herstart. - Het verwijderen van KernelSU (Root en alle modules) volledig en permanent. + Het verwijderen van KernelSU (root en alle modules) volledig en permanent. Herstel de standaard fabrieksimage (als er een back-up bestaat), die normaal gesproken vóór OTA wordt gebruikt. Als u KernelSU moet verwijderen, gebruikt u permanent verwijderen. Knipperen + Logboeken Opslaan Flash-succes Flash is mislukt Geselecteerde LKM: %s - Logboeken Opslaan + Actie Logs opgeslagen - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Sorteren (eerst ingeschakeld) + Sorteren (actie eerst) + De volgende modules worden geïnstalleerd: %1$s + Kan geen Superuser-toegang verlenen aan %s + su-compatibiliteit uitschakelen + Bevestigen + Schakel de mogelijkheid van een app om rootrechten te verkrijgen via de opdracht ⁠su uit (bestaande rootprocessen worden hierdoor niet beïnvloed). + Controleer op module-updates + Gebruik lokaal LKM-bestand + Alleen .ko bestanden worden ondersteund + Kernel umount uitschakelen + Schakel het umount-gedrag op kernelniveau uit dat wordt beheerd door KernelSU. + Verbeterde beveiliging inschakelen + Strengere beveiligingsbeleidsmaatregelen inschakelen. + Standaard + Tijdelijk inschakelen + Permanent inschakelen + Verwerken… + Trek omlaag om te vernieuwen + Vrijgeven om te vernieuwen + Verfrissend… + Succesvol vernieuwd diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 3a2203ae..276d3f23 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -1,15 +1,18 @@ + KernelSU Strona główna Nie zainstalowano Kliknij, aby zainstalować Działa - Wersja: %s + Wersja: %d + Superużytkownicy: %d + Moduły: %d Nieobsługiwany - KernelSU obsługuje obecnie tylko jądra GKI - Jądro - SuSFS Version + KernelSU obsługuje obecnie tylko jądra GKI. + Wersja jądra Wersja menedżera + Odcisk Status SELinux Wyłączony Enforcing @@ -20,8 +23,6 @@ Nie udało się wyłączyć modułu: %s Brak zainstalowanych modułów Moduły - Sortuj (najpierw działania) - Sortuj (najpierw włączone) Odinstaluj Instaluj Instaluj @@ -38,6 +39,7 @@ Nie udało się odinstalować: %s Wersja Autor + Moduły są niedostępne, ponieważ OverlayFS jest wyłączone przez jądro! Odśwież Pokaż aplikacje systemowe Ukryj aplikacje systemowe @@ -47,21 +49,25 @@ Moduły są niedostępne z powodu konfliktu z Magiskiem! Poznaj KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów + Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów. Wesprzyj nas KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę. - Join our %2$s channel]]> + Dołącz do kanału %2$s]]> + Profil aplikacji Domyślny Szablon Własny Nazwa profilu + Przestrzeń nazw montowania + Odziedziczona + Globalna + Indywidualna Grupy Uprawnienia Kontekst SELinux Odmontuj moduły Nie udało się zaktualizować profilu aplikacji dla %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Domyślnie odmontuj moduły + Odmontuj moduły Globalna wartość domyślna opcji \"Odmontuj moduły\" w profilu aplikacji. Jeśli jest włączona, wycofuje wszystkie modyfikacje dokonane przez moduły dla aplikacji, które nie mają ustawionego profilu. Włączenie tej opcji umożliwi KernelSU przywrócenie wszelkich zmodyfikowanych plików przez moduły dla tej aplikacji. Domena @@ -69,296 +75,84 @@ Zaktualizuj Pobieranie modułu: %s Rozpocznij pobieranie: %s - Nowa wersja %s jest dostępna. Kliknij, aby zaktualizować. + Nowa wersja %s jest dostępna. Kliknij, aby zaktualizować! Uruchom - Wymuś zatrzymanie + Wymuś zatrzymanie Restartuj Nie udało się zaktualizować reguł SELinux dla %s + Obecna wersja KernelSU %d jest zbyt stara, aby menedżer działał poprawnie. Prosimy o aktualizację do wersji %d lub nowszej! Dziennik zmian - Szablon profilu aplikacji - Zarządzaj lokalnym i internetowym szablonem profilu aplikacji - Stwórz szablon - Edytuj szablon - Identyfikator - Błędny identyfikator szablonu - Nazwa - Opis - Zapisz - Usuń - Zobacz szablon - Tylko do odczytu - Szablon o takim identyfikatorze już istnieje! - Importuj/Eksportuj - Importuj ze schowka - Eksportuj do schowka - Nie można znaleźć lokalnego szablonu do eksportu! - Zaimportowano pomyślnie - Synchronizuj internetowe szablony - Nie udało się zapisać szablonu - Schowek jest pusty! - Pobranie dziennika zmian nie powiodło się: %s - Wyszukaj aktualizacje - Wyszukuj aktualizacje automatycznie przy otwieraniu aplikacji - Nie udało się przyznać roota! - Akcja - Close - Włącz debugowanie WebView + Debugowanie WebView Może być użyte do debugowania WebUI. Włącz tylko w razie potrzeby. + Obraz partycji %1$s jest zalecany + Wybierz KMI + Dalej Instalacja bezpośrednia (zalecane) Wybierz plik Zainstaluj do nieaktywnego slotu (po aktualizcji OTA) Po ponownym uruchomieniu Twoje urządzenie zostanie **ZMUSZONE** do uruchomia się z obecnie nieaktywnego slotu! \nUżyj tej opcji dopiero po zakończeniu aktualizacji OTA. \nCzy chcesz kontynuować? - Dalej - Obraz partycji %1$s jest zalecany - Wybierz KMI - Odinstaluj - Odinstaluj tymczasowo + Stwórz szablon + Edytuj szablon + Nazwa + Opis + Zapisz + Usuń + Tylko do odczytu + Importuj/Eksportuj + Importuj ze schowka + Eksportuj do schowka + Nie można znaleźć lokalnego szablonu do eksportu! + Zaimportowano pomyślnie + Nie udało się zapisać szablonu + Schowek jest pusty! + Zarządzaj lokalnym i internetowym szablonem profilu aplikacji. + Synchronizuj internetowe szablony + Zobacz szablon + Błędny identyfikator szablonu + Szablon profilu aplikacji + Identyfikator + Szablon o takim identyfikatorze już istnieje! + Nie udało się przyznać roota! + Otwórz + Pobranie dziennika zmian nie powiodło się: %s + Wyszukaj aktualizacje + Wyszukuj aktualizacje automatycznie przy otwieraniu aplikacji. Odinstaluj zupełnie Przywróć obraz fabryczny + Odinstaluj tymczasowo + Odinstaluj Tymczasowo odinstaluj KernelSU, przywróć do oryginalnego stanu po następnym ponownym uruchomieniu. - Całkowite i trwałe odinstalowanie KernelSU (Root i wszystkich modułów). + Całkowite i trwałe odinstalowanie KernelSU (root i wszystkich modułów). Przywróć obraz fabryczny (jeśli istnieje kopia zapasowa), zwykle używany przed OTA; jeśli chcesz odinstalować KernelSU, użyj opcji \"Odinstaluj całkowicie\". Flashowanie Flashowanie ukończone pomyślnie Flashowanie nieudane Wybrano LKM: %s Zapisz dzienniki + Akcja Dzienniki zapisane - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + Sortuj (najpierw działania) + Sortuj (najpierw włączone) + Potwierdź + Wyłącz możliwość uzyskania uprawnień roota przez dowolną aplikację za pomocą polecenia su (nie wpłynie to na istniejące procesy roota). + Zainstalowane zostaną następujące moduły: %1$s + Wyłącz zgodność z su + Nie można przyznać dostępu superużytkownika do %s + Użyj lokalnego pliku LKM + Obsługiwane są wyłącznie pliki .ko + Wyłącz odmontowywanie jądra + Wyłącz odmontowywanie z poziomu jądra, kontrolowane przez KernelSU. + Przetwarzanie… + Przeciągnij w dół, aby odświeżyć + Uwolnij, aby odświeżyć + Odświeżanie… + Odświeżono pomyślnie + Sprawdź aktualizację modułów + Włącz rozszerzone zabezpieczenia + Włącz bardziej rygorystyczne zasady bezpieczeństwa. + Domyślnie + Włącz tymczasowo + Włącz na stałe diff --git a/manager/app/src/main/res/values-pt-rBR/strings.xml b/manager/app/src/main/res/values-pt-rBR/strings.xml index a53077df..aa13f600 100644 --- a/manager/app/src/main/res/values-pt-rBR/strings.xml +++ b/manager/app/src/main/res/values-pt-rBR/strings.xml @@ -4,11 +4,14 @@ Não instalado Clique para instalar Em execução - Versão: %s + Versão: %d + SuperUsuários: %d + Módulos: %d Sem suporte KernelSU suporta apenas kernels GKI agora - Kernel + Versão do kernel Versão do gerenciador + Impressão digital Status do SELinux Desativado Impondo @@ -35,6 +38,7 @@ Não foi possível desinstalar %s Versão Autor + Os módulos estão indisponíveis porque OverlayFS está desabilitado pelo kernel! Atualizar Mostrar apps do sistema Ocultar apps do sistema @@ -47,10 +51,16 @@ Saiba como instalar o KernelSU e usar os módulos Apoie-nos KernelSU sempre foi e sempre será, gratuito e de código aberto. No entanto, você pode nos ajudar enviando uma pequena doação. + Junte-se ao nosso canal do %2$s]]> + Perfil do Aplicativo Padrão Modelo Personalizado Nome do perfil + Montar namespace + Herdado + Global + Individual Grupos Capacidades Contexto do SELinux @@ -66,9 +76,10 @@ Começando a baixar %s Nova versão %s está disponível, clique para atualizar. Iniciar - Forçar parada + Forçar parada Reiniciar Falha ao atualizar as regras do SELinux para %s + A versão atual do KernelSU %d é muito baixa para o gerenciador funcionar corretamente. Por favor, atualize para a versão %d ou superior! Registro de alterações Importado com sucesso Exportar para a área de transferência @@ -92,9 +103,10 @@ Excluir A área de transferência está vazia! Ver modelo - Verificar por atualização + Verificar por atualizações Verifique automaticamente se há atualizações ao abrir o app Falha ao conceder acesso root! + Abrir Ativar depuração do WebView Pode ser usado para depurar o WebUI. Por favor, ative somente quando necessário. Selecione um arquivo @@ -122,4 +134,9 @@ Registros salvos Ordenar (Ação primeiro) Ordenar (Ativado primeiro) + Desative temporariamente a capacidade de qualquer app obter privilégios root por meio do comando ⁠su (processos root existentes não serão afetados). + Desativar compatibilidade su + Confirmar + Não foi possível conceder acesso de SuperUsuário a %s + Os seguintes módulos serão instalados: %1$s diff --git a/manager/app/src/main/res/values-pt/strings.xml b/manager/app/src/main/res/values-pt/strings.xml index cda81c7a..f5b50fde 100644 --- a/manager/app/src/main/res/values-pt/strings.xml +++ b/manager/app/src/main/res/values-pt/strings.xml @@ -1,362 +1,83 @@ - Início Não instalado + Início Clique para instalar Funcionando - Versão: %s + Super Usuário: %d + Módulos: %d + Versão: %d + Kernel + Instalar Sem suporte KernelSU suporta apenas kernels GKI agora - Kernel - Versão SuSFS - Versão do aplicativo Status do SELinux + Versão do aplicativo + Falha ao desativar o módulo: %s + Impressão digital Desabilitado Impondo Permissivo Desconhecido Super Usuário Falha ao ativar o módulo: %s - Falha ao desativar o módulo: %s Nenhum módulo instalado - Modulos - Ordenar (exceto primeiro) - Ordenar (Habilitado primeiro) Desinstalar - Instalar - Instalar + Modulos Reiniciar - Configurações + Instalar Reinicialização Suave - Reiniciar modo recuperação + Configurações Reinicializar modo Bootloader - Reiniciar para baixar - Reiniciar em EDL - Sobre - Tem certeza de que deseja desinstalar o módulo %s\? - %s desinstalado + Reiniciar modo recuperação Falha ao desinstalar: %s Versão Autor Atualizar - Mostrar aplicativos do sistema Esconder apps do sistema + Reiniciar para baixar + Reiniciar em EDL + Tem certeza de que deseja desinstalar o módulo %s\? + Sobre + overlayfs não está disponível, o módulo não pode funcionar! Enviar log - Modo de segurança - Reinicie para entrar em vigor - Os módulos estão desativados porque estão em conflito com os do Magisk! - Aprender KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html + %s desinstalado + Mostrar aplicativos do sistema Aprenda a instalar o KernelSU e usar os módulos - Apoie-nos O KernelSU é, e sempre será, gratuito e de código aberto. No entanto, você pode nos mostrar que se importa fazendo uma doação. - Entre em nosso canal no %2$s]]> + Veja o código-fonte em %1$s
Junte-se ao nosso canal %2$s
+ Individual + Global + Herdado Padrão Modelo Personalizado Nome do perfil + Montar namespace + Modo de segurança + Reinicie para entrar em vigor + Aprender KernelSU + Os módulos estão desativados porque estão em conflito com os do Magisk! + Apoie-nos Grupos Capacidades contexto SELinux + Domínio + Atualização Desativar modulos Falha ao atualizar o perfil do aplicativo para %s - A versão atual do KernelSU %s é muito baixa para o gerenciador funcionar corretamente. Atualize para a versão %s ou superior! Módulos desativados por padrão O valor padrão global para \"Módulos Umount\" em Perfis de Aplicativos. Se ativado, removerá todas as modificações de módulo do sistema para aplicativos que não possuem um Perfil definido. - Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este aplicativo. - Domínio Regras - Atualização - Baixando módulo: %s + Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este aplicativo. Iniciar o download: %s - Nova versão: %s está disponível, clique para baixar - Lançar - Forçar parada - Reiniciar + Baixando módulo: %s Falha ao atualizar as regras do SELinux para: %s - Registro de alterações - Modelo do Perfil do Aplicativo - Gerenciar modelo local e online do perfil do aplicativo - Criar modelo - Editar modelo - ID - ID de modelo incorreta - Nome - Descrição - Salvar - Apagar - Ver template - Somente leitura - O ID do modelo já existe! - Importar/Exportar - Importar da área de transferência - Exportar para área de transferência - Não foi possível encontrar modelo local para exportar! - Importado com sucesso - Sincronizar modelos on-line - Falha ao salvar o modelo - A área de transferência está vazia! - Falha ao buscar o registro de alterações: %s - Verificar atualização - Verifique automaticamente se há atualizações ao abrir o app - Falha ao conceder acesso root! - Ações - Fechar - Ativar depuração do WebView - Pode ser usado para depurar o WebUI. Por favor, ative somente quando necessário. - Instalação direta (recomendada) - Selecione uma imagem que precisa ser corrigida - Instalar no slot inativo (após o OTA) - Seu dispositivo será **FORÇADO** para ligar no slot inativo atual após uma reinicialização!\nUse esta opção apenas depois que OTA for finalizado.\nContinuar? - Avançar - A imagem da partição %1$s é recomendada - Selecionar KMI - Desinstalar - Desinstalar temporariamente - Desinstalar permanentemente - Restaurar imagem de fábrica - Desinstale temporariamente o KernelSU e restaure ao estado original após a próxima reinicialização - Desinstale o KernelSU (root e todos os módulos) completamente e permanentemente - Restaure a imagem de fábrica (se existir um backup), geralmente usada antes do OTA. Se você precisar desinstalar o KernelSU, use \"Desinstalar permanentemente\". - Flasheando - Flash bem-sucedido - Flash falhou - LKM selecionado: %s + https://kernelsu.org/guide/what-is-kernelsu.html + Reiniciar + Lançar + Forçar parada + Nova versão: %s está disponível, clique para baixar + A versão atual do KernelSU %d é muito baixa para o gerenciador funcionar corretamente. Atualize para a versão %d ou superior! Salvar Registros - Registros salvos - - ¿confirmar la instalación del módulo %1$s? - módulo desconocido - - Confirmar Restauração de Módulo - Esta operação irá sobrescrever todos os módulos existentes. Continuar? - Confirmação - Cancelar - - Backup bem sucedido (tar.gz) - Backup falhou: %1$s - módulos de backup - restaurar módulos - - Módulos restaurados com sucesso, reinicialização necessária - Falha na restauração: %1$s - Reiniciar agora - Erro desconhecido - - A execução do comando falhou: %1$s - - Backup concluído - Falha no backup permitido: %1$s - Confirmar Restauração de Lista - Esta operação irá sobrescrever a lista de permissão atual. Continuar? - Allowlist restaurada com sucesso - Falha na restauração do Allowlist: %1$s - Backup Permitido - Restaurar Allowlist - Fundo personalizado do App - Selecione uma imagem como plano de fundo - Transparência da barra de navegação - Android Version: - Modelo do aparelho - Conceder superusuário à %s não é permitido - Desativar compatibilidade com su - Temporariamente desativar qualquer aplicativo de obter privilégios de superusuário através do comando Wait su (os processos raiz existentes não serão afetados). - Tem certeza que deseja instalar os seguintes módulos %1$d ? \n\n%2$s - More settings - SELinux - Ativado - Desabilitado - Modo de simplicidade - Oculta cartas desnecessárias quando ativadas - Ocultar versão kernel - Ocultar versão kernel - Ocultar outras informações - Oculta informações sobre o número de super usuários, módulos e módulos do KPM na página inicial - Ocultar status SuSFS - Ocultar informações de status SuSFS na página inicial - Ocultar status do link - Ocultar informações do cartão do link na página inicial - Tema - Seguir o sistema - Claro - Escuro - Hook manual - Cor dinâmica - Cores dinâmicas usando temas de sistema - Escolha uma cor temática - Blue - Verde - Violeta - Laranja - Rosa - Cinza - Amarelo - Install Anykernel3 - Instalar o arquivo kernel AnyKernel3 - Requer privilégios de superusuário - Esboço completo - Reiniciar imediatamente? - Sim - Não - Reinicialização falhou - KPM - Não há módulos do kernel instalados neste momento - Versão - Autor - Desinstalar - Desinstalado com sucesso - Falha ao desinstalar - Carregamento do módulo kpm com sucesso - Falha ao carregar o módulo kpm - Parâmetros - Executar - Versão do KPM - Fechar - As seguintes funções do módulo de kernel foram desenvolvidas pelo KernelPatch e modificadas para incluir as funções do módulo de kernel do SukiSU Ultra - SukiSU Ultra aguarda ansiosamente - Sucesso - Falhou - SukiSU Ultra será uma ramificação relativamente independente da KSU no futuro, mas ainda apreciamos o KernelSU e o MKSU, etc. para suas contribuições! - Sem suporte - Apoie-nos - Kernel não corrigido - Kernel não configurado - Configurações personalizadas - KPM Install - Carga - Embutir - Por favor seleccione: %1\$s Modo de instalación del Módulo \n\nCarga: Cargar temporalmente el módulo \nInsertar: Instalar permanentemente en el sistema - Não foi possível verificar se o arquivo do módulo existe - Cor do tema - Tipo de arquivo incorreto! Selecione o arquivo .kpm. - Desinstalar - O seguinte KPM será desinstalado: %s - Use dois dedos para ampliar a imagem e um dedo para arrastá-la para ajustar a posição - Restituição - - Flash concluído - - Preparando… - limpando arquivos… - Copiando arquivo… - Extraindo a ferramenta flash… - Atualizando o script flash… - Flasheando kernel… - Flash completo - - Selecionar Slot em Flash - Por favor, selecione o slot de destino para flashear a inicialização - Slot B - Slot B - Slot selecionado: %1$s - Obtendo o espaço original - Configurando o slot especificado - Restaurar Slot Padrão - Slot padrão do sistema atual:%1$s - - Falha ao copiar - Você parece estar convertendo de um formato com degradação para um sem perdas. Esteja ciente que a perda de qualidade não pode ser desfeita, então este processo não melhorará a qualidade do áudio e provavelmente aumentará o tamanho do arquivo. Continuar assim mesmo? - Flash falhou - - LKM reparo/instalação - Flasheando kernel - Kernel - Usando a ferramenta de correção:%1$s - Configurar - Configurações Do Aplicativo - Ferramentas - - Aplicativo não encontrado - SELinux habilitado - SELinux Desativado - Falha na alteração de estado do SELinux - Configurações Avançadas - Personaliza a barra de ferramentas. - Retorno - Fundo definido com sucesso - Remover - Ícone alternativo - Alterar o ícone do launcher para o ícone do KernelSU. - Ícone alterado - - Exibir função KPM - Oculta as informações e funções do KPM na barra inicial e inferior - - Selecione o mecanismo de WebUI para usar - Seleção automática - Forçar o uso de WebUI X - Uso obrigatório do KSU WebUI - Injetar Eruda na WebUI X - Injetar um console de depuração na WebUI X para facilitar a depuração. Requer depuração da web para estar ligada. - - DPI aplicado - Ajustar a densidade de exibição da tela apenas para o aplicativo atual - Pequeno - Média - Grande - extra grande - Customizável - Aplicando configurações de DPI - Confirmar alteração de DPI - Tem certeza que deseja alterar o DPI do aplicativo do %1$d para %2$d? - O aplicativo precisa ser reiniciado para aplicar as novas configurações DPI, não afeta a barra de status do sistema ou outras aplicações - DPI foi definido para %1$d, efetivo após reiniciar o aplicativo - - Língua do aplicativo - Padrão do sistema - Ajuste da escuridão do cartão - - Código de erro - Por favor, verifique o log - Módulo sendo instalado %1$d/%2$d - %d Falha ao instalar um novo módulo - Falha ao baixar módulo - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Mais votados - Parte Inferior - Selecionado - opção - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-ro/strings.xml b/manager/app/src/main/res/values-ro/strings.xml index 10d9095b..6149f955 100644 --- a/manager/app/src/main/res/values-ro/strings.xml +++ b/manager/app/src/main/res/values-ro/strings.xml @@ -4,12 +4,14 @@ Nu este instalat Click pentru a instala Funcționează - Versiune: %s + Versiune: %d + Super-utilizatori: %d + Module: %d Necompatibil KernelSU suportă doar nuclee GKI acum Nucleu - SuSFS Version Versiune Manager + Amprentă Stare SELinux Dezactivat Obligatoriu @@ -20,8 +22,6 @@ Dezactivarea modulului %s a eșuat Niciun modul instalat Module - Sort (Action first) - Sort (Enabled first) Dezinstalează Instalează Instalează @@ -38,6 +38,7 @@ Dezinstalare eșuată: %s Versiune Autor + overlayfs nu este disponibil, modulul nu poate funcționa! Reîmprospătează Arată aplicațiile de sistem Ascunde aplicațiile de sistem @@ -50,17 +51,20 @@ Află cum să instalezi KernelSU și să utilizezi module Suport KernelSU este, și va fi întotdeauna, gratuit și cu codul sursă deschis. Cu toate acestea, ne poți arăta că îți pasă făcând o donație. - Join our %2$s channel]]> + Alătură-te canalului nostru %2$s]]> Implicit Șablon Personalizat Nume profil + Montare spațiu de nume + Moștenit + Global + Individual Grupuri Capabilități Context SELinux Module u-montate Nu s-a putut actualiza profilul aplicației pentru %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! U-montează modulele în mod implicit Valoarea implicită globală pentru „Module u-montate” în Profilurile aplicațiilor. Dacă este activat, va elimina toate modificările modulelor aduse sistemului pentru aplicațiile care nu au un profil setat. Activarea acestei opțiuni va permite KernelSU să restaureze orice fișiere modificate de către modulele pentru această aplicație. @@ -70,295 +74,59 @@ Se descarcă modulul: %s Începe descărcarea: %s Versiune nouă: %s este disponibilă, clic pentru a actualiza - Lansare - Oprire forțată - Repornește Nu s-au reușit actualizările regulilor SELinux pentru: %s + Lansare + Oprire forțată + Repornește + Versiunea actuală a KernelSU %d este prea mică pentru ca managerul să funcționeze corect. Actualizează la versiunea %d sau o versiune superioară! Jurnalul modificărilor - Șablon de profil al aplicației - Gestionează șablonul local și online al Profilului aplicației - Creează un șablon - Editează șablonul - ID - ID șablon nevalid - Nume - Descriere - Salvează - Șterge - Vizualizare șablon - doar citire - ID-ul șablonului există deja! - Import/Export - Import din clipboard + Importat cu succes Export în clipboard Nu există șabloane locale de exportat! - Importat cu succes - Sincronizează șabloanele online - Nu s-a salvat șablonul - Clipboard-ul este gol! + ID-ul șablonului există deja! + Import din clipboard Preluarea jurnalului de modificări a eșuat: %s + Nume + ID șablon nevalid + Sincronizează șabloanele online + Creează un șablon + doar citire + Import/Export + Nu s-a salvat șablonul + Editează șablonul + ID + Șablon de profil al aplicației + Descriere + Salvează + Gestionează șablonul local și online al Profilului aplicației + Șterge + Clipboard-ul este gol! + Vizualizare șablon Verifică actualizarea Se verifică automat actualizările când deschizi aplicația - Nu s-a acordat acces root! - Action - Close Activează depanarea WebView Poate fi folosit pentru a depana WebUI, activează numai când este necesar. - Instalare directă (recomandat) - Selectează un fișier - Instalează într-un slot inactiv (după OTA) + Nu s-a acordat acces root! + Deschide + Se recomandă imaginea partiției %1$s + Înainte Dispozitivul va fi **FORȚAT** să pornească în slot-ul inactiv curent după o repornire! \nFolosește această opțiune numai după finisarea OTA. \nContinui? - Înainte - Se recomandă imaginea partiției %1$s Selectează KMI + Instalare directă (recomandat) + Selectează un fișier + Instalează într-un slot inactiv (după OTA) Dezinstalează - Dezinstalează temporar - Dezinstalează definitiv Restaurare imagine stoc Dezinstalează temporar KernelSU, se revine la starea inițială după următoarea repornire. + Lkm selectat: %s + Dezinstalează temporar + Dezinstalează definitiv Dezinstalare KernelSU (Root și toate modulele) complet și permanent. Restaurează imaginea stoc din fabrică (dacă există o copie de rezervă), utilizată de obicei înainte de OTA; dacă trebuie să dezinstalezi KernelSU, utilizează „Dezinstalare permanentă”. Instalare Instalare reușită Instalarea a eșuat - Lkm selectat: %s Salvează Jurnale - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 1c6aedec..951b5a24 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -4,24 +4,26 @@ Не установлен Нажмите, чтобы установить Работает - Версия: %s + Версия: %d + Суперпользователи: %d + + Модули: %d Не поддерживается - Драйвера KernelSU не найдены в ядре, не то ядро? - Ядро - Статус SuSFS + KernelSU поддерживает только GKI ядра. + Версия ядра Версия менеджера + Подпись Состояние SELinux Выключен Принудительный Разрешающий Неизвестно - Рут-доступ + Суперпользователи + Не удалось включить модуль %s Не удалось отключить модуль %s Нет установленных модулей Модули - Сортировать (Сначала с действием) - Сортировать (Сначала включённые) Удалить Установить Установка @@ -38,7 +40,8 @@ Не удалось удалить %s Версия Автор - Обновить + Модули недоступны, так как OverlayFS отключен ядром! + Обновить страницу Показать системные приложения Скрыть системные приложения Отправить логи @@ -47,697 +50,111 @@ Модули недоступны из-за конфликта с Magisk! Узнайте о KernelSU https://kernelsu.org/ru_RU/guide/what-is-kernelsu.html - Узнайте, как установить KernelSU и использовать модули + Узнайте, как установить KernelSU и использовать модули. Поддержите нас KernelSU был и всегда будет бесплатным и открытым проектом. Однако Вы всегда можете поддержать нас, отправив небольшое пожертвование. - Присоединяйтесь к нашему %2$s каналу]]> + Присоединяйтесь к нашему %2$s каналу]]> + App Profile + По умолчанию Шаблон Пользовательский Название профиля + Монтировать пространство имен + Унаследованный + Глобальный + Индивидуальный Группы Возможности Контекст SELinux Размонтировать модули - Не удалось обновить профиль приложения для %s - Текущая версия KernelSU %s слишком низкая для правильной работы менеджера. Пожалуйста, обновите до версии %s или выше! - Размонтировать модули по умолчанию - Глобальное значение по умолчанию для \"Размонтировать модули\" в профиле приложения. При включении будут удалены все модификации модулей в системе для приложений, у которых не задан профиль - Включение этой опции позволит KernelSU восстанавливать любые измененные модулями файлы для данного приложения. + Не удалось обновить App Profile для %s + Размонтировать модули + Глобальное значение по умолчанию для \"Размонтировать модули\" в App Profile. При включении будут удалены все модификации модулей в системе для приложений, у которых не задан Profile. + Включение этой опции позволит KernelSU восстанавливать любые изменённые модулями файлы для данного приложения. Домен Правила Обновить Скачивание модуля: %s Начало скачивания: %s - Новая версия: %s доступна, нажмите чтобы скачать. - Запустить - Остановить принудительно - Перезапустить + Новая версия: %s доступна, нажмите чтобы скачать! + Остановить принудительно Не удалось обновить правила SELinux для %s + Запустить + Перезапустить + Текущая версия KernelSU %d слишком низкая для правильной работы менеджера. Пожалуйста, обновите до версии %d или выше! Список изменений - Шаблон профиля приложения - Управление локальным и онлайн-шаблоном профиля приложения - Создать шаблон - Редактирование шаблона - Идентификационный номер - Неверный ID шаблона - Название - Описание - Сохранить - Удалить - Просмотр шаблона - Только чтение - Шаблон с таким ID уже существует! - Импорт/Экспорт - Импортировать из буфера обмена + Успешный импорт Экспортировать в буфер обмена Нет локальных шаблонов для экспорта! - Успешный импорт - Синхронизировать онлайн-шаблоны - Не удалось сохранить шаблон - Буфер обмена пуст! + Шаблон с таким ID уже существует! + Импортировать из буфера обмена Не удалось получить список изменений: %s - Проверка обновлений - Автоматически проверять обновления при открытии приложения + Название + Неверный ID шаблона + Синхронизировать онлайн-шаблоны + Создать шаблон + Только чтение + Импорт/Экспорт + Не удалось сохранить шаблон + Редактирование шаблона + Идентификационный номер + Шаблон профиля приложения + Описание + Сохранить + Управление локальным и онлайн-шаблоном профиля приложения. + Удалить + Буфер обмена пуст! + Просмотр шаблона + Проверять наличие обновлений + Автоматическая проверка обновлений при открытии приложения. Не удалось выдать root! - Действие - Закрыть - Включить отладку WebView + Открыть + Отладка WebView Используется для отладки WebUI. Пожалуйста, включайте только при необходимости. Прямая установка (Рекомендуется) - Выбрать файл Установка в неактивный слот (После OTA) + Далее + Выбрать файл Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки! \n Используйте эту опцию только после завершения OTA. \n Продолжить? - Далее - Выбрать раздел - Использовать локальный файл LKM - Поддерживаются только файлы .ko - Образ раздела %1$s рекомендуется Выбрать KMI - Удалить + %1$s образ раздела рекомендуется Удалить на время + Удалить KernelSU (root и все модули) полностью. Удалить полностью + Временно удалить KernelSU, восстановить исходное состояние после следующей перезагрузки + Удалить Восстановить сток образ - Временно удалить KernelSU, восстановить исходное состояние после следующей перезагрузки. - Удалить KernelSU (рут и все модули) полностью. Восстановить исходный заводской образ (если существует резервная копия), обычно используется перед OTA; если вам нужно удалить KernelSU, используйте «Удалить полностью». - Установка Установка выполнена + Установка Установка не выполнена Выбран LKM: %s Сохранить логи + Действие Логи сохранены - - подтвердите установку модуля %1$s? - неизвестный модуль - - Подтвердите восстановление модуля - Эта операция перезапишет все существующие модули. Продолжить? - Подтвердить - Отмена - - Резервная копия создана успешно - Ошибка резервного копирования: %1$s - Резервное копирование модулей - Восстановить модули - - Модули восстановлены, требуется перезагрузка - Ошибка восстановления: %1$s - Перезапустить сейчас - Неизвестная ошибка - - Выполнение команды не удалось: %1$s - - Резервная копия создана успешно - Ошибка резервного копирования списка: %1$s - Подтвердите восстановление списка - Эта операция перезапишет текущий список разрешений. Продолжить? - Список успешно восстановлен - Не удалось восстановить список: %1$s - Резервное копирование списка - Восстановить список - Пользовательский фон приложения - Выберите изображение в качестве фона - Прозрачность панели навигации - Версия Android - Модель устройства - Предоставление рут-доступа %s запрещено + Сортировать (Сначала с действием) + Сортировать (Сначала включённые) Отключить su совместимость - Временно отключить все приложения от получения рут-доступа через su команду (запущенные процессы с рут-доступом не будут затронуты). + Отключить возможность получения root привилегий любым приложениям с помощью команды su (существующие root процессы не будут затронуты). + Будут установлены следующие модули: %1$s + Подтвердить + Не удалось предоставить права Суперпользователя к %s + Обновлено успешно + Обновление… + Отпустите чтобы обновить + Потяните вниз чтобы обновить + Обработка… + Отключить поведение размонтирования на уровне ядра, контролируемое KernelSU. Отключить размонтирование ядра - Временное отключение умного уровня ядра KernelSU. + Использовать локальный LKM файл + Поддерживаются только .ko файлы Включить повышенную безопасность Включить более строгие политики безопасности. По умолчанию - Временно включены - Постоянно включены - Уверены, что хотите установить следующие %1$d модули? \n\n%2$s - Доп. настройки - SELinux - Включено - Выключен - Режим простоты - Скрывать ненужные карточки при запуске - Скрыть версию ядра - Скрывать версию ядра на главной странице - Скрыть другую информацию - Скрывать информацию о количестве приложений с рут-доступом, модулей и KPM-модулей на главной странице - Скрыть статус SuSFS - Скрывать информацию о статусе SuSFS на главной странице - Скрыть статус Zygisk - Скрывать информацию об имплементации Zygisk на главной странице - Скрыть карточки со ссылками - Скрывать карточки со ссылками на главной странице - Скрыть строки модуля - Скрывать имя пакета и размер в карточке модуля - Тема - Как в системе - Светлая - Тёмная - Самопис. хуки - Динамические цвета - Использовать акцентные цвета системы - Выберите цвет темы - Синий - Зеленый - Фиолетовый - Оранжевый - Розовый - Серый - Желтый - Установка AnyKernel3 - Прошить файл ядра AnyKernel3 - Требуется root-права - Скрабровка завершена - Будет ли перезагрузка немедленно? - Да - Нет - Не удалось перезагрузиться - KPM - На данный момент нет установленных модулей ядра - Версия - Автор - Удалить - Удаление завершено - Не удалось удалить - KPM модуль успешно загружен - Ошибка загрузки KPM модуля - Параметры - Выполнить - Статус KPM - Закрыть - Следующие модульные функции ядра были разработаны KernelPatch и изменены для включения в него функций модуля ядра SukiSU Ultra - Будущее SukiSU Ultra - Успешно - Провален - В будущем SukiSU Ultra будет относительно независимым ответвлением KSU, но мы по-прежнему ценим официальный KernelSU и MKSU и т.д. за их вклад! - Не поддерживается - Поддерживается - Ядро не изменено - Ядро не настроено - Персонализация - Установка KPM - Загрузить - Код для вставки - Пожалуйста, выберите %1\$s режим установки модуля \n\nЗагрузить: Временно загрузить модуль \nВстроенный: Окончательно установить в систему - Не удалось проверить наличие файла модуля - Цвет темы - Неверный тип файла! Пожалуйста, выберите .kpm файл. - Удалить - Следующие KPM будут удалены: %s - Используйте два пальца для увеличения изображения, и один палец для изменения положения - Реализация - - Прошивка завершена - - Подготовка… - Очистка файлов… - Копирование файлов… - Извлечение флэш инструмента… - Изменение флэш-скрипта… - Прошивка ядра… - Прошивка завершена - - Выберите слот для прошивки - Пожалуйста, выберите целевой слот для прошивки загрузки - Слот A - Слот B - Выбран LKM: %1$s - Получение оригинальной ячейки - Установка указанного слота - Восстановить - Текущий слот по умолчанию:%1$s - - Копирование не удалось - Неизвестная ошибка - Установка не выполнена - - Починить/установить LKM - Прошить AnyKernel3 - Ядро - С помощью инструмента патрулирования:%1$s - Настройка - Настройки приложения - Инструменты - - Приложение не найдено - SELinux включен - SELinux отключен - Сбой изменения статуса SELinux - Расширенные - Внешний вид - Возвращение - Фон успешно установлен - Пользовательский фон удалён - Альт. иконка - Поменять иконку приложения на иконку KernelSU - Иконка поменяна - - Скрыть статус KPM - Скрывать информацию и функции KPM на домашней странице и панели навигации - - Движок WebUI - Автовыбор - WebUI X - KSU WebUI - Вставить Eruda в WebUI X - Вставьте консоль отладки в WebUI X для упрощения отладки. Для этого требуется включение веб-отладки. - - Изменить DPI - Регулировка плотности экрана только для текущего приложения - маленький - средний - большой - огромный - настраиваемый - Применить настройки DPI - Подтвердите изменение DPI - Вы уверены, что хотите изменить DPI приложения с %1$d на %2$d? - Приложение нужно перезапустить, чтобы применить новые настройки DPI. Это не влияет на системную строку состояния или другие приложения - DPI был установлен в %1$d, действующий после перезапуска приложения - - Язык приложения - Как в системе - Затемнение карточек - - код ошибки - Пожалуйста, проверьте журнал - Модуль устанавливается %1$d/%2$d - %d не удалось установить новый модуль - Ошибка загрузки модуля - Прошить ядро - - Все - Рут-доступ - Пользовательские - Стандартные - - Название (по возрастанию) - Название (по убыванию) - Время установки (новые) - Время установки (старые) - Размер (по убыванию) - Размер (по возрастанию) - Частота использования - - В этой категории нет приложений - - Доступ отклонён - Доступ выдан - Размонтирование модулей - Отключить монтирование модуля - Развернуть - Свернуть - Наверх - Вниз - Выбранные - опция - - Настройки списка - Сортировка - Выбор типа приложения - - Конфигурация SuSFS - Описание конфигурации - Эта функция позволяет вам настраивать переменную uname для SuSFS. Впишите значение и нажмите \"Применить\" для изменения. - Значение uname - Пожалуйста, введите пользовательское значение uname - Подмена времени сборки - Пожалуйста, введите значение для подмены времени сборки - Текущее значение: %s - Текущее время сборки: %s - Значение по умолчанию - Применить - - Подтвердить сброс - - Не удалось найти файл ksu_susfs - Выполнение команды SuSFS не удалось - Ошибка выполнения команды SuSFS: %s - Значение uname для SuSFS установлено успешно: %s - - Конфигурация SuSFS - - Автозапуск - Автоматически применять все конфигурации не по умолчанию при перезагрузке - Для включения необходимо добавить конфигурацию - Не удалось включить автозапуск - Не удалось отключить автозапуск - Ошибка автозапуска конфигурации: %s - Нет доступных конфигураций для автозапуска - - Базовые настройки - SUS пути - SUS монтирование - Попробовать размонтировать - Настройки пути - Статус включённых функций - - Добавить SUS путь - Добавить SUS монтирование - Добавить попробовать размонтировать - Путь SUS успешно добавлен - Ошибка пути - Путь - Путь монтирования - например: /system/addon.d - SUS пути не настроены - SUS монтирование не настроено - Попытка размонтировать не настроена - - Режим размонтирования - Обычное размонтирование (0) - Размонтирование отсоединением (1) - Нормальный - Отсоединить - Режим: %1$s (%2$s) - Попытка размонтировать путь успешно добавлена: %s - Попытка размонтировать путь успешно сохранена: %s - - - Сбросить SUS пути - Это очистит все конфигурации пути SUS. Вы уверены, что хотите продолжить? - Сброс SUS монтирования - Это очистит все конфигурации SUS монтирования. Вы уверены, что хотите продолжить? - Сбросить Umount - Это очистит все конфигурации размонтирования. Вы уверены, что хотите продолжить? - Сбросить настройки пути - - Путь к данным Android - Путь к SD-карте - Задать путь к данным Android - Задать путь к SD-карте - - Показывать текущее состояние функций SuSFS - Информация о состоянии объектов не найдена - Включено - Выключено - - Поддержка SUS пути - Поддержка SUS монтирования - Поддержка размонтирования - Поддержка подмены uname - Подмена Cmdline/Bootconfig - Поддержка Open Redirect - Поддержка логов - Автомонтирование по умолчанию - Автоматическое бинд монтирование - Автоматически попробовать размонтировать привязать монтировать - Скрытие KSU SUSFS Symbols - Поддержка SUS Kstat - Функция переключения режима SUS SU - - Настраиваемые функции SuSFS - SuSFS включить лог - Включить или отключить логирование для SuSFS - SuSFS настройка логирования - Включить логирование SuSFS - Выключить логирование SuSFS - JSON обновления - JSON ссылка обновления скопирована в буфер обмена - - Показать больше информации о модуле - Показывать доп. информацию о модулях, такую как JSON ссылку для обновления - Место выполнения - Текущее место выполнения: %s - Сервис - Post-FS-Data - Выполнить после запуска системных сервисов - Выполнить после монтирования файловой системы, но перед полной загрузкой. Может вызвать бутлуп - Информация о слоте - Просмотреть информацию о текущем слоте загрузки и скопировать значения - Текущий слот: %s - Uname: %s - Время сборки: %s - Текущее - Использовать Uname - Использовать время сборки - Не удается получить информацию о слоте - - SuSFS автозапуск модуля включен, путь к модулям: %s - SuSFS модуль автозапуска отключен - - Конфигурация Kstat - Добавлена статическая конфигурация Kstat: %1$s - Конфигурация Kstat удалена: %1$s - Путь к Kstat добавлен: %1$s - Путь к Kstat удалён: %1$s - Kstat обновлен: %1$s - Полный клон Kstat обновлён: %1$s - Добавить статическую конфигурацию Kstat - Путь к файлу/папке - Подсказка: Вы можете использовать «по умолчанию» для использования оригинального значения - Добавить путь Kstat - Добавить - Сбросить конфигурацию Kstat - Вы уверены, что хотите очистить все конфигурации Kstat? Это действие нельзя отменить. - Описание конфигурации Kstat - • add_sus_kstat_staticall: Статическая статистика информации о файлах/директориях - • add_sus_kstat: Добавить путь перед привязкой, сохраняя исходную статистику - • update_sus_kstat: Обновление цели, сохранение размера и блоков без изменений - • update_sus_kstat_full_clone: Обновление только новых, сохранить другие исходные значения - Статическая конфигурация Kstat - Управление путями Kstat - Пока нет конфигурации Kstat, нажмите кнопку выше, чтобы добавить - - Контроль скрытия SUS монтирования - Управление режимом скрытия SUS монтирования для процессов - Скрыть монтирования SUS для всех процессов - Когда включено, монтирования SUS будут скрыты от всех процессов, включая процессы KSU - Если отключено, монтирования SUS будут скрываться только из процессов, не связанных с KSU, процессы KSU могут видеть монтирования - Включено скрытие монтирования SUS для всех процессов - Отключено скрытие монтирования SUS для всех процессов - Рекомендуется отключить после разблокировки экрана или во время service.sh или boot-completed.sh, так как это должно исправить проблему на некоторых root-приложениях, которые опираются на монтирование, смонтированное процессом KSU - Текущие настройки: %s - Скрыть для всех процессов - Скрыть только для процессов, не связанных с KSU - Скрыть доп. информацию о ядре - Включить или отключить чистый режим, отображаемой версии ядра SukiSU - Путь к данным Android был установлен на: %s - Путь к SD-карте был установлен на: %s - Установка пути может быть не вполне успешной, но SUS пути будут добавлены - - Резервное копирование - Создайте резервную копию всех конфигураций SuSFS. Файл резервной копии будет содержать все настройки, пути и конфигурации. - Создать резервную копию - Резервная копия успешно создана: %s - Не удалось создать резервную копию: %s - Файл резервной копии не найден - Неверный формат файла резервной копии - Несоответствие версии резервного копирования - Восстановить - Восстановить настройки SuSFS из файла резервной копии. Это перезапишет все текущие настройки. - Выберите файл резервной копии - Конфигурация успешно восстановлена из резервной копии, созданной на %s с устройства: %s - Ошибка восстановления: %s - Подтвердить восстановление - Это очистит все конфигурации размонтирования. Вы уверены, что хотите продолжить? - Восстановление - Дата резервного копирования: %s - Устройство: %s - Версия: %s - Заблокированное состояние - Переопределить свойство состояния блокировки загрузчика в режиме службы late_start - Очистка - Очистка остаточных файлов и каталогов различных модулей и инструментов (может быть удален по ошибке, в результате потери и неспособности начаться, используйте с осторожностью) - Редактировать путь SUS - Редактировать точку монтирования SUS - Изменить попробовать размонтировать - Изменить статическую конфигурацию Kstat - Редактировать путь Kstat - Сохранить - Редактировать - Удалить - Обновить - Обновить конфиг Kstat - Обновить путь Kstat - Полное обновление клона SuSFS - Размонтировать сервис изоляции Zygote - Включите эту опцию, чтобы размонтировать точки монтирования Zygote при запуске системы - Размонтирование служб Zygote включено - Размонтирование служб Zygote выключено - Путь к приложению - Другие пути - Другое - Приложение - Добавить путь приложения - Несоответствие версий библиотеки SuSFS! Ядро: %1$s, менеджер: %2$s. Рекомендуется обновить ядро или менеджер - Внимание - Поиск приложений - Выбрано %1$d приложений - %1$d приложений уже добавлено - Все приложения были добавлены - Конфигурация динамической подписи - Включено (размер: %s) - Выключено - Включить динамическую подпись - Размер подписи - Хэш подписи - Хеш должен содержать 64 шестнадцатеричных символа - Конфигурация динамической подписи успешно установлена - Не удалось установить конфигурацию динамической подписи - Неверная конфигурация подписи - Динамическая подпись отключена - Не удалось очистить динамическую подпись - Динамическая - Подпись %1$d - Неизвестно - Активный менеджер - Нет активного менеджера - SukiSU - Реализация Zygisk - - Пути цикла SUS - Добавить путь SUS - Редактировать путь SUS - Путь SUS успешно добавлен: %1$s - Удален путь цикла SUS: %1$s - Обновлен путь цикла SUS: %1$s -> %2$s - SUS пути не настроены - Сбросить SUS пути - Вы уверены, что хотите очистить все пути цикла SUS? Это действие нельзя отменить. - Циклический путь - /data/example/path - Примечание: Только пути НЕ внутри /storage/ и /sdcard/ могут быть добавлены по цикловым путям. - Ошибка: пути не могут быть внутри /storage/ или /sdcard/ каталогов - Циклические пути - Добавить циклический путь - - Конфигурация пути цикла - Пути цикла повторно отмечены как SUS_PATH в каждом пользовательском приложении, не являющемся root, или изолированном запуске службы. Это помогает решить проблемы, в которых добавленные пути могут иметь сброс статуса inode или повторно созданные inode в ядре. - Спуф AVC лога - Спуф AVC лога включен - Спуф AVC лога отключен - отключен: Отключить спуффинг sus tcontext от \'su\' показывать в Avc логе в ядре.\n включен: Включить спуффинг sus tcontext от \'su\' с \'ядром\' показывать в Avc логе в ядре - Важное замечание:\n -- Значение по умолчанию в ядре установлено как \'0\'\n -- Включение этой опции иногда затрудняет разработчикам определение причины при отладке проблем, связанных с разрешениями или SELinux, поэтому пользователям рекомендуется отключать её во время отладки - - Проверенные - Подпись модуля подтверждена - Подтверждение подписи - При установке модуля принудительно проверять подпись. (Только для архитектуры ARM) - Неизвестный издатель - Неподписанные модули могут быть неполными. Для защиты устройства установка этого модуля была заблокирована. - Неподписанные модули могут быть неполными. Вы хотите разрешить установку на этом устройстве следующего модуля от неизвестного издателя? - Типы хуков - - Патч KPM - Добавление дополнительных функций KPM - Патч KPM - Применить патч KPM к образу ядра перед прошивкой - Патч для отмены KPM - Отменить ранее применённый патч KPM - Патч KPM включен - Патч для отмены KPM включен - Режим патча KPM - Режим отмены патча KPM - - Подготовка инструментов KPM - Применение патча KPM - Отмена патча KPM - Найден файл образа: %s - Патч KPM успешно применен - Патч KPM успешно отменён - Файл успешно переупакован - - Не удалось извлечь zip-файл - Файл образа не найден - Ошибка патча KPM - Ошибка патча отмены KPM - Ошибка патча KPM: %s - - Следовать за ядром - Использовать ядро без каких-либо изменений KPM - - Пользовательский режим сканирования списка приложений - Включение этой опции позволит использовать сканирование пользовательского режима для списка приложений, улучшая стабильность. (Если вы столкнетесь с такими проблемами, как замораживание во время сканирования ядра списка приложений, вы можете включить эту опцию.) - Поиск многопользовательских приложений - Когда включено, сканирует приложения для всех пользователей, включая рабочие профили - Не удалось установить, проверьте права доступа - Очистить среду Runtime - Очистить среду Runtime и остановить службу сканирования - Вы уверены, что хотите очистить среду Runtime? Это остановит службу сканирования и удалит связанные с ней файлы. - Runtime окружение успешно очищено - Не удалось очистить среду runtime - - Подтвердите установку - Подтвердите установку (%d файлов) - Установить - Модули - Ядро - Неизвестно - Неизвестное ядро - Неизвестный файл - Версия - Автор - Описание - Поддерживаемые устройства - - SUS Maps - Путь к библиотеке - /data/adb/modules/my_module/zygisk/arm64-v8a.so - Добавить SUS Map - Изменить SUS Map - SUS map успешно добавлен: %1$s - SUS map удален: %1$s - SUS map обновлен: %1$s -> %2$s - SUS maps не настроены - Сбросить SUS Maps - Это действие удалит все настроенные SUS maps. Это действие нельзя отменить. - Скрытие физической памяти - Скрыть настоящий файл mmapped с разных карт в /proc/self/ - Скрыть реальные пути к папкам памяти из /proc/self/[maps|smaps_rollup|map_files|mem|pagemap]. Обратите внимание: эта функция не поддерживает скрытие анонимных карт и может скрыть встроенные хуки или PLT хуки вызванные инъекцией самой библиотеки. - Важное уведомление: для приложений с хорошо реализованными механизмами обнаружений эта функция может работать не эффективно . - Сначала найдите PID и UID целевого приложения, используя ps -enf, затем проверьте соответствующие пути в /proc/<pid>/maps и сравните номера устройства с номерами в /proc/1/mountinfo. Функция скрытия будет работать должным образом только если номера устройств совпадают. - - Просмотр логов - Назад - Поиск - Очистить логи - Вы уверены, что хотите удалить выбранный файл журнала? Это действие нельзя отменить. - Логи успешно очищены - Фильтрация по типу - Все типы - Показаны записи %1$d из %2$d - Логи не найдены - Совпадающих журналов не найдено - Обновить - Необработанный лог - Поиск по UID, команде или деталям… - Очистить поиск - Просмотр логов использования - Просмотр журналов доступа к KernelSU - Исключить подтипы - Текущее приложение - Страница: %1$d/%2$d | Всего журналов: %3$d - Слишком много журналов, отображаются только последние записи %1$d - Загрузить больше логов - Все логи отображаются - - Подтвердите удаление SukiSU Manager? - Удаление не повлияет на функциональность вашего root-доступа. Он продолжит работать от этого менеджера. - Текущий менеджер несовместим с этим ядром! Пожалуйста, обновите ядро до версии %2$d или выше (сейчас %1$d) - - Управление путями размонтирования - Управления путями размонтирования ядра - Для применения изменений необходима перезагрузка. Система применит новый конфиг при следующем запуске. - Добавить путь размонтирования - Смонтировать путь - Флаги размонтирования - 0=Обычное размонтирование, 8=MNT_DETACH, -1=Автоматически - Флаги - Подтвердите удаление - Вы уверены, что хотите удалить путь размонтирования%s? - Путь добавлен, изменения вступят в силу после перезагрузки - Путь удален, изменения вступят в силу после перезагрузки - Неудача - Подтвердите действие - Вы уверены, что хотите удалить все кастомные пути? (Стандартные пути не будут удалены) - Кастомные пути удалены - Удалить кастомные пути - Применить конфигурацию - Конфигурация применена к ядру + Включить временно + Включить постоянно + Проверять обновления модулей diff --git a/manager/app/src/main/res/values-sl/strings.xml b/manager/app/src/main/res/values-sl/strings.xml index 27bf3718..2a0a6518 100644 --- a/manager/app/src/main/res/values-sl/strings.xml +++ b/manager/app/src/main/res/values-sl/strings.xml @@ -1,362 +1,106 @@ - Domov - Ni nameščeno Klikni za namestitev V obdelavi - Verzija: %s - Ne podpira + Verzija: %d + Superuporabniki: %d KernelSU podpira samo GKI kernele Kernel - SuSFS Version Verzija upravitelja + Prstni odtis SELinux status Onemogočeno - Enforcing - Permissive Neznano - SuperUporabnik - Napaka pri omogočanju modula: %s Napaka pri onemogočanju modula: %s Ni nameščenih modulov Modul - Sort (Action first) - Sort (Enabled first) Odmesti Namesti Namesti - Znova zaženi - Nastavitve Mehki ponovni zagon Ponovni zagon v Recovery Ponovni zagon v Bootloader - Ponovni zagon v Download Ponovni zagon v EDL - O nas Ste prepričani, da želite odstraniti modul %s? %s je odmeščen - Napaka pri odmeščanju: %s - Verzija Avtor - Osveži - Prikaz sistemskih aplikacij + overlayfs ni na voljo, modul ne more delovati! Skrij prikaz sistemskih aplikacij Prijavite dnevnik - Varni način - Za uveljavitev je potreben ponovni zagon - Moduli so onemogočeni, ker so v konfliktu z Magiskovimi! Naučite se KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Naučite se, kako namestiti KernelSU in uporabiti module - Podprite nas - KernelSU je, in bo vedno brezplačen in odprtokoden. Kljub temu, nam lahko z donacijo pokažete, da vam je mar. - Join our %2$s channel]]> + Glej odprto kodo na %1$s
Pridružite se našem %2$s kanalu
Privzeto Predloga + Imenski prostor vmestitve + Podedovano + Globalno + Pozameznik + Zmožnosti + Izvrzi module + Po privzetem izvrzi module + Domena + Posodobitev + Nalaganje modula: %s + Zaženi + Ponovni zagon + Dnevnik sprememb + Predloga za aplikacijski profil + Domov + Moduli: %d + Ne podpira + SuperUporabnik + Napaka pri omogočanju modula: %s + Znova zaženi + Nastavitve + Ponovni zagon v Download + O nas + Verzija + Napaka pri odmeščanju: %s + Osveži + Varni način + Za uveljavitev je potreben ponovni zagon + Prikaz sistemskih aplikacij + Moduli so onemogočeni, ker so v konfliktu z Magiskovimi! + Podprite nas Po meri Ime profila Skupine - Zmožnosti SELinux kontekst - Izvrzi module + KernelSU je, in bo vedno brezplačen in odprtokoden. Kljub temu, nam lahko z donacijo pokažete, da vam je mar. Napaka pri posodobitvi aplikacijskega profila za %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Po privzetem izvrzi module + Za pravilno funkionalnost upravitelja je trenutna KernelSU verzija %d prenizka. Potrebna je nadgradnja na verzijo %d ali več! Globalno privzeta vrednost za \"Izvrzi module\" v aplikacijskih profilih. Če je omogočena, bo to odstranilo vse sistemske modifikacije modulov za aplikacije, ki nimajo nastavljenega profila. Omogočanje te opcije bo dovolilo KernelSU, da obnovi vse zaradi modulov spremenjene datoteke za to aplikacijo. - Domena + Prisilna ustavitev Pravila - Posodobitev - Nalaganje modula: %s Začni z nalaganjem: %s Na voljo je nova verzija: %s, kliknite za nadgradnjo - Zaženi - Prisilna ustavitev - Ponovni zagon Napaka pri posodobitvi SELinux pravil za: %s - Dnevnik sprememb - Predloga za aplikacijski profil - Upravljaj z lokalnimi in spletnimi predlogami za aplikacijski profil + Ni nameščeno + Enforcing + Permissive Ustvari predlogo Uredi predlogo - id Neveljaven id predloge - Ime Opis Shrani Odstrani - Ogled predloge le za branje id predloge že obstaja! - Uvoz/Izvoz Uvoz iz odložišča Izvoz v odložišče Lokalna predloga za izvoz ni bila najdena! - Uvoz uspešen - Sinhroniziraj predloge iz spleta Napaka pri shranjevanju predloge Odložišče je prazno! + Upravljaj z lokalnimi in spletnimi predlogami za aplikacijski profil + id + Ime + Ogled predloge + Uvoz uspešen + Sinhroniziraj predloge iz spleta + Uvoz/Izvoz Napaka pri pridobivanju dnevnika sprememb: %s - Check update - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed. - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot. - Uninstalling KernelSU (Root and all modules) completely and permanently. - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". - Flashing - Flash success - Flash failed - Selected LKM: %s Shrani Dnevnike - Logs saved - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - -
diff --git a/manager/app/src/main/res/values-sr/strings.xml b/manager/app/src/main/res/values-sr/strings.xml index cf1e1a45..73920b6f 100644 --- a/manager/app/src/main/res/values-sr/strings.xml +++ b/manager/app/src/main/res/values-sr/strings.xml @@ -1,9 +1,11 @@ + Superkorisnici + Moduli: %d Додирните да бисте инсталирали Почетна Није инсталирано - Верзија: %s + Верзија: %d Ради Сачувај Дневнике \ No newline at end of file diff --git a/manager/app/src/main/res/values-te/strings.xml b/manager/app/src/main/res/values-te/strings.xml index 7a9c943b..9887318f 100644 --- a/manager/app/src/main/res/values-te/strings.xml +++ b/manager/app/src/main/res/values-te/strings.xml @@ -14,6 +14,8 @@ ఇన్‌స్టాల్ చేయలేదు ఇన్‌స్టాల్ చేయడానికి క్లిక్ చేయండి పని చేస్తోంది - వెర్షన్: %s + వెర్షన్: %d + సూపర్‌యూజర్‌లు: %d + మాడ్యూల్స్: %d లాగ్‌లు సేవ్ చేయండి diff --git a/manager/app/src/main/res/values-th/strings.xml b/manager/app/src/main/res/values-th/strings.xml index ed07c50f..13aa68ff 100644 --- a/manager/app/src/main/res/values-th/strings.xml +++ b/manager/app/src/main/res/values-th/strings.xml @@ -4,43 +4,44 @@ ยังไม่ได้ติดตั้ง กดเพื่อติดตั้ง กำลังทำงาน - เวอร์ชัน: %s - ไม่รองรับ - ตอนนี้ KernelSU รองรับเคอร์เนลประเภท GKI เท่านั้น - เคอร์เนล - SuSFS Version + เวอร์ชัน: %d เวอร์ชันตัวจัดการ - สถานะ SELinux - ปิดใช้งาน + สิทธิ์ผู้ใช้ขั้นสูง: %d + โมดูล: %d + ไม่รองรับ Enforcing + รีบูตเข้าสู่โหมดกู้คืน + ซอฟต์รีบูต + KernelSU รองรับเคอร์เนลประเภท GKI เท่านั้น + เวอร์ชันเคอร์เนล + ปิดใช้งาน + ลายนิ้วมือ + สถานะ SELinux Permissive ไม่ทราบ สิทธิ์ผู้ใช้ขั้นสูง - ไม่สามารถเปิดใช้งานโมดูล %s - ไม่สามารถปิดใช้งานโมดูล: %s + ล้มเหลวในการเปิดใช้งานโมดูล %s + ล้มเหลวในการปิดใช้งานโมดูล: %s ไม่มีโมดูลที่ติดตั้ง โมดูล - เรียงลำดับ (แบบรันคำสั่งก่อน) - เรียงลำดับ (แบบเปิดใช้งานก่อน) ถอนการติดตั้ง + ตั้งค่า ติดตั้ง ติดตั้ง รีบูต - ตั้งค่า - ซอฟต์รีบูต - รีบูตเข้าสู่โหมดกู้คืน รีบูตเข้าสู่โหมด Bootloader + เกี่ยวกับ รีบูตเข้าสู่โหมด Download รีบูตเข้าสู่โหมด EDL - เกี่ยวกับ - คุณแน่ใจว่าจะถอนการติดตั้งโมดูล %s หรือไม่\? %s ถอนการติดตั้งสำเร็จ - ไม่สามารถถอนการติดตั้ง %s - เวอร์ชัน + ถอนการติดตั้ง %s ล้มเหลว + โมดูลไม่สามารถใช้งานได้เนื่องจาก OverlayFS ถูกปิดใช้งานโดยเคอร์เนล! + คุณแน่ใจว่าจะถอนการติดตั้งโมดูล %s หรือไม่\? ผู้สร้าง - รีเฟรช + เวอร์ชัน แสดงแอประบบ ซ่อนแอประบบ + รีเฟรช ส่ง logs โหมดปลอดภัย รีบูตเพื่อให้มีผล @@ -49,316 +50,107 @@ https://kernelsu.org/guide/what-is-kernelsu.html เรียนรู้วิธีการติดตั้ง KernelSU และวิธีใช้งานโมดูลต่าง ๆ สนับสนุนพวกเรา - KernelSU เป็นโอเพ่นซอร์สฟรีทั้งจากนี้และตลอดไป อย่างไรก็ตาม คุณสามารถแสดงความห่วงใยได้ด้วยการบริจาค - Join our %2$s channel]]> + KernelSU จะเป็นโอเพ่นซอร์สฟรีเสมอ อย่างไรก็ตาม คุณสามารถแสดงความห่วงใยได้ด้วยการบริจาค + และเข้าร่วมช่อง %2$s channel]]> + กำหนดเอง ค่าเริ่มต้น เทมเพลต - กำหนดเอง ชื่อโปรไฟล์ + Mount เนมสเปซ + ทั่วไป + สืบทอด + ส่วนบุคคล หมวดหมู่ ความสามารถของแอป + การเปิดใช้งานตัวเลือกนี้จะทำให้ KernelSU สามารถกู้คืนไฟล์ที่แก้ไขโดยโมดูลสำหรับแอปนี้ได้ บริบท SELinux Umount โมดูล ไม่สามารถอัปเดตโปรไฟล์แอปสำหรับ %s ได้ - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Umount โมดูลตามค่าเริ่มต้น - หากเปิดใช้งานค่าเริ่มต้นโดยทั่วไปสำหรับ \"Umount โมดูล\" ในโปรไฟล์แอป จะเป็นการลบการแก้ไขโมดูลทั้งหมดในระบบสำหรับแอปพลิเคชันที่ไม่มีการตั้งค่าโปรไฟล์ - การเปิดใช้งานตัวเลือกนี้จะทำให้ KernelSU สามารถกู้คืนไฟล์ที่แก้ไขโดยโมดูลสำหรับแอปนี้ได้ + Umount โมดูล โดเมน - กฎ อัปเดต + กฎ กำลังดาวน์โหลดโมดูล: %s กำลังเริ่มดาวน์โหลด: %s - เวอร์ชันใหม่: %s พร้อมใช้งาน คลิกเพื่ออัปเกรด - เปิด - บังคับหยุด + เวอร์ชัน %s พร้อมใช้งาน คลิกเพื่ออัปเกรด ! + บังคับหยุด รีสตาร์ท + หากเปิดใช้งานค่าเริ่มต้นโดยทั่วไปสำหรับ \"Umount โมดูล\" ในโปรไฟล์แอป จะเป็นการลบการแก้ไขโมดูลทั้งหมดในระบบสำหรับแอปพลิเคชันที่ไม่มีการตั้งค่าโปรไฟล์ + เปิด ไม่สามารถอัปเดตกฎ SElinux สำหรับ %s + KernelSU เวอร์ชัน %d ต่ำเกินไป ทำให้ตัวจัดการไม่สามารถทำงานได้อย่างถูกต้อง โปรดอัปเกรดเป็นเวอร์ชัน %d หรือสูงกว่า! บันทึกการเปลี่ยนแปลง - เทมเพลตโปรไฟล์แอป - จัดการเทมเพลตโปรไฟล์แอปในเครื่องและเทมเพลตออนไลน์ - สร้างเทมเพลต - แก้ไขเทมเพลต - ไอดี - ไอดีเทมเพลตไม่ถูกต้อง - ชื่อ - คำอธิบาย - บันทึก - ลบ - ดูเทมเพลต - อ่านเท่านั้น - มีไอดีเทมเพลตนี้อยู่แล้ว! - นำเข้า/ส่งออก - นำเข้าจากคลิปบอร์ด + นำเข้าเสร็จสิ้น ส่งออกไปยังคลิปบอร์ด ไม่พบเทมเพลตในเครื่องที่จะส่งออก! - นำเข้าเสร็จสิ้น + มีไอดีเทมเพลตนี้อยู่แล้ว! + นำเข้าจากคลิปบอร์ด + ชื่อ + ไอดีเทมเพลตไม่ถูกต้อง ซิงค์เทมเพลตออนไลน์ + สร้างเทมเพลต + อ่านเท่านั้น + นำเข้า/ส่งออก ไม่สามารถบันทึกเทมเพลต + แก้ไขเทมเพลต + ไอดี + เทมเพลตโปรไฟล์แอป + คำอธิบาย + บันทึก + จัดการเทมเพลตโปรไฟล์แอปในเครื่องและเทมเพลตออนไลน์ + ลบ คลิปบอร์ดว่างเปล่า! + ดูเทมเพลต ดึงข้อมูลบันทึกการเปลี่ยนแปลงล้มเหลว: %s + เปิด + ไม่สามารถให้สิทธิ์รูทได้! ตรวจสอบการอัปเดต ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อเปิดแอป - ไม่สามารถให้สิทธิ์รูทได้! - คำสั่ง - Close - เปิดใช้งานการแก้ไขข้อบกพร่อง WebView - ใช้เพื่อดีบัก WebUI เท่านั้น โปรดเปิดใช้งานเมื่อจำเป็น - ติดตั้งโดยตรง (แนะนำ) + การแก้ไขข้อบกพร่อง WebView + เลือก KMI + ต่อไป เลือกไฟล์ ติดตั้งลงในสล็อตที่ไม่ใช้งาน (หลังจาก OTA) + ติดตั้งโดยตรง (แนะนำ) + แนะนำให้ใช้อิมเมจพาร์ติชัน %1$s + ใช้เพื่อดีบัก WebUI เท่านั้น โปรดเปิดใช้งานเมื่อจำเป็น อุปกรณ์ของคุณจะถูก **บังคับ** ให้บูตไปยังสล็อตที่ไม่ได้ใช้งานหลังจากรีบูต! \nโปรดใช้ตัวเลือกนี้หลังจาก OTA เสร็จแล้วเท่านั้น \nดำเนินการต่อหรือไม่? - ต่อไป - แนะนำให้ใช้อิมเมจพาร์ติชัน %1$s - เลือก KMI - ถอนการติดตั้ง - ถอนการติดตั้งชั่วคราว ถอนการติดตั้งถาวร เรียกคืนอิมเมจดั้งเดิม ถอนการติดตั้ง KernelSU ชั่วคราว จะคืนค่าเป็นสถานะดั้งเดิมหลังจากรีบูตในครั้งถัดไป - การถอนการติดตั้ง KernelSU (การรูทและโมดูลทั้งหมด) อย่างสมบูรณ์โดยถาวร - คืนค่าโรงงานอิมเมจดั้งเดิม (หากมีข้อมูลสำรอง) ส่วนใหญ่มักใช้ก่อนทำการ OTA ซึ่งหากคุณต้องการถอนการติดตั้ง KernelSU โปรดใช้ \"ถอนการติดตั้งถาวร\" กำลังแฟลช แฟลชสำเร็จ แฟลชล้มเหลว เลือก LKM: %s + ถอนการติดตั้ง + ถอนการติดตั้งชั่วคราว + การถอนการติดตั้ง KernelSU (การรูทและโมดูลทั้งหมด) อย่างสมบูรณ์ + คืนค่าโรงงานอิมเมจดั้งเดิม (หากมีข้อมูลสำรอง) ส่วนใหญ่มักใช้ก่อนทำการ OTA ซึ่งหากคุณต้องการถอนการติดตั้ง KernelSU โปรดใช้ \"ถอนการติดตั้งถาวร\" บันทึกบันทึก + คำสั่ง บันทึก Log แล้ว - - confirm install module %1$s? - unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - backup modules - restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (existing root processes will not be affected). - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - ปิดการใช้งาน - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides information about the number of super users, modules and KPM modules on the home page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Link Card Status - Hide link card information on the home page - Theme - Follow system - Light - Dark - Manual Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file. - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current Slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon. - Icon switched - - Display KPM Function - Display KPM information and Function in home and bottom bar (Need to reopen the app) - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on. - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - oversize - customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (new) - Installation time (old) - descending order of size - ascending order of size - frequency of use - - No application in this category - - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - option - - Menu Options - Sort by - Application Type Selection - - - - - - - - - - - - - - - - + เรียงลำดับ (แบบรันคำสั่งก่อน) + เรียงลำดับ (แบบเปิดใช้งานก่อน) + ปิดใช้งานความสามารถของแอปต่าง ๆ ในการรับสิทธิ์ root โดยใช้คำสั่ง ⁠su (กระบวนการ root ที่มีอยู่จะไม่ได้รับผลกระทบ) + โมดูล %1$s จะถูกติดตั้ง + ยืนยัน + ไม่สามารถให้สิทธิ์ผู้ใช้ขั้นสูงกับ %s ได้ + ปิดใช้งานความเข้ากันได้ของ su + ตรวจสอบการอัปเดตโมดูล + ใช้ไฟล์ LKM ในเครื่อง + รองรับเฉพาะไฟล์ .ko เท่านั้น + ปิดใช้งานการ umount เคอร์เนล + ปิดใช้งานพฤติกรรมการ umount ในระดับเคอร์เนลควบคุมโดย KernelSU + เปิดใช้งานการรักษาความปลอดภัยขั้นสูง + เปิดใช้งานนโยบายความปลอดภัยที่เข้มงวดยิ่งขึ้น + ค่าเริ่มต้น + เปิดใช้งานชั่วคราว + เปิดใช้งานถาวร + กำลังประมวลผล… + ดึงลงเพื่อรีเฟรช + ปล่อยเพื่อรีเฟรช + กำลังรีเฟรช… + รีเฟรชสำเร็จ diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 3ee11121..4f7a5bb2 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -1,721 +1,143 @@ + KernelSU Ana Sayfa - Yüklü değil - Yüklemek için tıklayın + Kurulmadı + Kurmak için tıklayın Çalışıyor - Sürüm: %s + Sürüm: %d + Süper kullanıcılar: %d + Modüller: %d Desteklenmiyor - Kernel\'inizde KernelSU sürücüsü algılanmadı, yanlış çekirdek mi? - Kernel sürümü - SuSFS Sürümü + KernelSU şimdilik sadece GKI çekirdeklerini destekliyor + Çekirdek Versiyonu Yönetici sürümü + Parmak izi SELinux durumu Devre dışı - Enfocing(Etkin) - Permissive(Devre Dışı) + Etkin (Enforcing) + Serbest (Permissive) Bilinmiyor - SüperKullanıcı + Süper kullanıcı Modül etkinleştirilemedi: %s Modül devre dışı bırakılamadı: %s - Yüklü modül yok + Kurulu modül yok Modül - Sırala (Action önce) - Sırala (Etkin önce) Kaldır - Yükle - Yükle - Yeniden başlat + Kur + Kur + Cihazı yeniden başlat Ayarlar - Yumuşak yeniden başlatma - Kurtarma moduna yeniden başlat - Bootloader\'a yeniden başlat - Download moduna yeniden başlat - EDL moduna yeniden başlat + Hızlı yeniden başlat + Kurtarma modunda yeniden başlat + Önyükleyici modunda yeniden başlat + İndirme modunda yeniden başlat + EDL modunda yeniden başlat Hakkında %s modülünü kaldırmak istediğinizden emin misiniz? %s kaldırıldı - Kaldırılamadı: %s + Kaldırma başarısız: %s Sürüm Geliştirici + OverlayFS çekirdek tarafından devre dışı bırakıldığı için modüller kullanılamıyor! Yenile Sistem uygulamalarını göster Sistem uygulamalarını gizle Günlükleri gönder Güvenli mod - Etkili olması için yeniden başlatın + Değişikliklerin uygulanması için cihazı yeniden başlat Magisk ile çakışma nedeniyle modüller kullanılamıyor! KernelSU\'yu öğrenin https://kernelsu.org/guide/what-is-kernelsu.html - KernelSU\'yu nasıl yükleyeceğinizi ve modülleri nasıl kullanacağınızı öğrenin + KernelSU\'nun nasıl kurulacağını ve modüllerin nasıl kullanılacağını öğrenin Bizi destekleyin - KernelSU, her zaman olduğu gibi ücretsiz ve açık kaynaklıdır. Ancak bir bağış yaparak bizi destekleyebilirsiniz. - %2$s kanalımıza katılın]]> + KernelSU ücretsiz ve açık kaynaklı bir yazılımdır ve her zaman öyle kalacaktır. Ancak bağış yaparak bize destek olduğunuzu gösterebilirsiniz + %2$s kanalımıza katılın.]]> + Uygulama profili Varsayılan Şablon Özel Profil adı + Ad alanını bağla + Kalıtsal + Küresel + Bireysel Gruplar - Yetenekler - SELinux bağlamı - Modülleri bağlamayı kaldır - %s için Uygulama Profili güncellenemedi - Mevcut KernelSU sürümü %s, yöneticinin düzgün çalışması için çok düşük. Lütfen sürüm %s veya daha yüksek bir sürüme yükseltin! - Modülleri varsayılan olarak bağlamayı kaldır - Uygulama Profilindeki \"Modülleri bağlamayı kaldır\" için küresel varsayılan değer. Etkinleştirilirse, profil ayarlanmamış uygulamalar için sistemdeki tüm modül değişikliklerini kaldırır. - Bu seçeneği etkinleştirmek, KernelSU\'nun bu uygulama için modüller tarafından değiştirilen dosyaları geri yüklemesine izin verecektir. - Etki alanı + Yetkinlikler + SELinux içeriği + Modüllerin bağlantısını kes + %s için uygulama profili güncellenemedi + Mevcut KernelSU sürümü %d, yöneticinin düzgün çalışabilmesi için çok düşük. Lütfen %d sürümüne veya daha yüksek bir sürüme güncelleyin! + Varsayılan olarak modüllerin bağlantısını kes + Uygulama profilindeki \"Modüllerin bağlantısını kes\" seçeneği için varsayılan değer. Etkinleştirilirse, profil ayarı yapılmamış uygulamalar için modüllerin sistemde yaptığı tüm değişiklikler kaldırılacaktır + Bu seçeneği etkinleştirmek, KernelSU\'nun bu uygulama için modüller tarafından değiştirilen dosyaları geri yüklemesine izin verir + İsim alanı Kurallar Güncelle Modül indiriliyor: %s - İndirme başlıyor: %s - Yeni sürüm %s mevcut, güncellemek için tıklayın. - Başlat - Zorla durdur - Yeniden başlat + İndirme başladı: %s + Yeni sürüm: %s kullanılabilir, güncellemek için tıklayın + Uygulamayı başlat + Uygulamayı durmaya zorla + Uygulamayı yeniden başlat %s için SELinux kuralları güncellenemedi - Değişiklik günlüğü - Uygulama Profili Şablonu - Uygulama Profil Şablonunu yerel ve çevrimiçi olarak yönetin + Değişiklik geçmişi + Uygulama profili şablonu + Yerel ve çevrimiçi uygulama profili şablonlarını yönetin Şablon oluştur Şablonu düzenle Kimlik Geçersiz şablon kimliği - Ad + İsim Açıklama Kaydet Sil Şablonu görüntüle Salt okunur Şablon kimliği zaten var! - İçe/Dışa Aktar - Pano\'dan içe aktar - Pano\'ya dışa aktar - Dışa aktarılacak yerel şablon bulunamadı! + İçe aktar/Dışa aktar + Panodan içe aktar + Panodan dışa aktar + Dışa aktarmak için yerel şablon bulunamadı! Başarıyla içe aktarıldı Çevrimiçi şablonları senkronize et Şablon kaydedilemedi - Panodaki veri boş! - Değişiklik günlüğü alılamadı: %s - Güncelleme kontrolü - Uygulama açıldığında otomatik olarak güncellemeleri kontrol et - Root yetkisi verilemedi! - Eylem - Kapat - WebView hata ayıklama etkinleştir - WebUI\'yi hata ayıklamak için kullanılabilir. Sadece ihtiyaç duyulduğunda etkinleştirin. - Doğrudan yükleme (Önerilen) - Yamalanacak bir görüntü seçin - Etkin olmayan yuvaya yükle (OTA sonrası) - Cihazınız, yeniden başlatma sonrasında **ZORUNLU** olarak mevcut etkin olmayan yuvaya önyükleme yapacaktır!\nSadece OTA tamamlandıktan sonra bu seçeneği kullanın.\nDevam etmek istiyor musunuz? - İleri - Yerel LKM dosyası kullan - Yalnızca .ko dosyaları desteklenir - %1$s bölüm görüntüsü önerilir + Pano boş! + Değişiklik geçmişi alınamadı: %s + Güncellemeleri denetle + Uygulamayı açarken güncellemeleri otomatik denetle + Root izni verilemedi! + + Web görünümü hata ayıklamasını etkinleştir + Web kullanıcı arayüzünde hata ayıklamak için kullanılabilir, lütfen yalnızca gerektiğinde etkinleştirin + Doğrudan kur (Tavsiye edilen) + Bir dosya seçin + Etkin olmayan yuvaya kur (OTA\'dan sonra) + Cihazınız geçerli etkin olmayan yuvaya **ZORLA** yeniden başlatılacaktır! +\nBu seçeneği yalnızca OTA tamamlandıktan sonra kullanın. +\nDevam edilsin mi? + Sonraki KMI seçin - Kaldır + %1$s bölüm imajı önerilir Geçici olarak kaldır Kalıcı olarak kaldır - Fabrika görüntüsünü geri yükle - KernelSU\'yu geçici olarak kaldırın, bir sonraki yeniden başlatmadan sonra orijinal duruma geri dönün. - KernelSU\'yu (Root ve tüm modüller) tamamen ve kalıcı olarak kaldırın. - Fabrika görüntüsünü geri yükleyin (yedek varsa), genellikle OTA öncesi kullanılır; KernelSU\'yu kaldırmak istiyorsanız lütfen \"Kalıcı olarak kaldır\" seçeneğini kullanın. - Kurulum işlemi - Kurulum başarılı - Kurulum başarısız - Seçilen LKM: %s - Günlükleri kaydet + Stok imajı geri yükle + KernelSU\'yu geçici olarak kaldır, bir sonraki yeniden başlatmadan sonra orijinal durumuna geri yükle + Kaldır + KernelSU (Root ve tüm modüller) tamamen ve kalıcı olarak kaldırılıyor + Stok fabrika imajını geri yükler (eğer yedek varsa), genellikle OTA\'dan önce kullanılır; KernelSU\'yu kaldırmanız gerekiyorsa, lütfen \"Kalıcı olarak kaldır\" seçeneğini kullanın + Flaşlama başarılı + Seçili LKM: %s + Flaşlanıyor + Flaşlama başarısız + Günlükleri Kaydet + Aksiyon Günlükler kaydedildi - - %1$s modülünü yüklemek istediğinizden emin misiniz? - Bilinmeyen modül - - Modül Geri Yüklemeyi Onayla - Bu işlem, mevcut tüm modülleri üzerine yazacaktır. Devam etmek istiyor musunuz? Onayla - İptal - - Yedekleme başarılı (tar.gz) - Yedekleme başarısız: %1$s - Modülleri yedekle - Modülleri geri yükle - - Modüller başarıyla geri yüklendi, yeniden başlatma gerekiyor - Geri yükleme başarısız: %1$s - Şimdi yeniden başlat - Bilinmeyen hata - - Komut yürütme başarısız: %1$s - - İzin verilenler listesi yedekleme başarılı - İzin verilenler listesi yedekleme başarısız: %1$s - İzin Verilenler Listesi Geri Yüklemeyi Onayla - Bu işlem, mevcut İzin verilen listesinin üzerine yazacaktır. Devam etmek istiyor musunuz? - İzin verilen listesi başarıyla geri yüklendi - İzin verilen listesi geri yükleme başarısız: %1$s - İzin verilen listesini yedekle - İzin verilen listesini geri yükle - Özel Uygulama Arka Planı - Bir görüntü seçin ve arka plan olarak ayarlayın - Geçiş çubuğu şeffaflığı - Android sürümü - Cihaz modeli - %s için süper kullanıcı yetkisi verilemiyor + %s için Superuser erişimi verilemedi + Herhangi bir uygulamanın ⁠su komutu aracılığıyla kök ayrıcalıkları elde etme yeteneğini geçici olarak devre dışı bırakın (mevcut kök işlemleri etkilenmeyecektir) + Aşağıdaki modüller yüklenecek: %1$s + Sırala (Action önce) + Sırala (Etkin olanlar önce) Su uyumluluğunu devre dışı bırak - Geçici olarak herhangi bir uygulamanın su komutu aracılığıyla root ayrıcalıkları elde etmesini devre dışı bırakır (mevcut root işlemleri etkilenmez). - Çekirdek ayırmasını devre dışı bırak - KernelSU tarafından kontrol edilen çekirdek seviyesindeki ayırma davranışını devre dışı bırakın. - Gelişmiş güvenliği etkinleştir - Daha katı güvenlik politikalarını etkinleştirin. - Varsayılan - Geçici olarak etkinleştir - Kalıcı olarak etkinleştir - Aşağıdaki %1$d modülü yüklemek istediğinizden emin misiniz? \n\n%2$s - Daha fazla ayar - SELinux - Etkin - Devre dışı - Basit mod - Açıldığında gereksiz kartları gizler - Çekirdek sürümünü gizle - Çekirdek sürümünü gizle - Diğer bilgileri gizle - Gezinme çubuğu sayfasında süper kullanıcı, modül ve KPM modülü sayısı hakkında bilgi veren kırmızı noktayı gizler - SuSFS durumunu gizle - Ana sayfadaki SuSFS durum bilgilerini gizle - Zygisk durumunu gizle - Ana sayfada Zygisk uygulama bilgisini gizle - Bağlantı Kartı Durumunu Gizle - Ana sayfadaki bağlantı kartı bilgilerini gizle - Modül etiket satırlarını gizle - Modül kartlarında klasör adı ve boyut etiketlerini gizle - Tema - Sistemi takip et - Açık - Koyu - Manuel Kanca - Dinamik renkler - Sistem temaları kullanarak dinamik renkler - Bir tema rengi seçin - Mavi - Yeşil - Mor - Turuncu - Pembe - Gri - Sarı - Anykernel3 yükle - AnyKernel3 çekirdek dosyasını yaz - Root ayrıcalıkları gerektirir - Temizleme tamamlandı - Hemen yeniden başlatmak istiyor musunuz? - Evet - Hayır - Yeniden başlatma başarısız - KPM - Şu anda yüklü çekirdek modülü yok - Sürüm - Geliştirici - Kaldır - Başarıyla kaldırıldı - Kaldırılamadı - Kpm modülü yükleme başarılı - Kpm modülü yükleme başarısız - Parametreler - Yürüt - KPM Sürümü - Kapat - Aşağıdaki çekirdek modül işlevleri KernelPatch tarafından geliştirilmiş ve SukiSU Ultra\'nın çekirdek modül işlevlerini içerecek şekilde değiştirilmiştir - SukiSU Ultra bekliyor - Başarılı - Başarısız - SukiSU Ultra, gelecekte KSU\'nun nispeten bağımsız bir dalı olacak, ancak yine de resmi KernelSU ve MKSU gibi katkılarından dolayı teşekkür ederiz! - Desteklenmiyor - Destekleniyor - Çekirdek yamalanmadı - Çekirdek yapılandırılmadı - Özel ayarlar - KPM Yükleme - Yükle - Göm - Lütfen seçin: %1$s Modül Yükleme Modu \n\nYükle: Modülü geçici olarak yükle \nGömülü: Kalıcı olarak sisteme yükle - Modül dosyasının varlığını kontrol edilemiyor - Tema Rengi - Yanlış dosya türü! Lütfen .kpm dosyasını seçin. - Kaldır - Aşağıdaki KPM kaldırılacak: %s - Görüntüyü yaklaştırmak için iki parmağınızı kullanın ve bir parmağınızla sürükleyerek konumu ayarlayın - Yeniden sağla - - Flash\'lama Tamamlandı - - Hazırlanıyor… - Dosyalar temizleniyor… - Dosyalar kopyalanıyor… - Yazma aracı çıkartılıyor… - Yazma betiği yamalanıyor… - Çekirdek yazılıyor… - Yazma tamamlandı - - Yazma Yuvası Seç - Lütfen boot yazma için hedef yuvayı seçin - Yuva A - Yuva B - Seçilen yuva: %1$s - Orijinal yuva alınıyor - Belirtilen yuva ayarlanıyor - Varsayılan Yuvayı Geri Yükle - Geçerli varsayılan sistem yuvası: %1$s - - Kopyalama başarısız - Bilinmeyen hata - Flash\'lama başarısız - - LKM onarımı/yükle - AnyKernel3 Flaşlama - Çekirdek sürümü:%1$s - Kullanılan yama aracı:%1$s - Yapılandır - Uygulama Ayarları - Araçlar - - Uygulama bulunamadı - SELinux Etkin - SELinux Devre Dışı - SELinux Durumu değiştirilemedi - Gelişmiş Ayarlar - Araç çubuğunu özelleştir - Geri - Arka plan başarıyla ayarlandı - Özel arka planlar kaldırıldı - Alternatif simge - Başlatıcı simgesini KernelSU simgesiyle değiştir. - Simge değiştirildi - - KPM İşlevini Gizle - Ana sayfa ve alt çubuktaki KPM bilgilerini ve işlevini gizler - - Kullanılacak WebUI motorunu seç - Otomatik Seçim - WebUI X kullanımını zorla - KSU WebUI kullanımını zorunlu kıl - WebUI X\'e Eruda enjekte et - WebUI X\'e bir hata ayıklama konsolu enjekte edin, böylece hata ayıklama daha kolay olur. Web hata ayıklamanın açık olması gerekir. - - Uygulanan DPI - Sadece geçerli uygulama için ekran görüntüleme yoğunluğunu ayarlayın - Küçük - Orta - Büyük - Aşırı büyük - Özelleştirilebilir - DPI ayarlarını uygula - DPI değişikliğini onayla - Uygulama DPI\'sini %1$d\'den %2$d\'ye değiştirmek istediğinizden emin misiniz? - Yeni DPI ayarlarını uygulamak için uygulamanın yeniden başlatılması gerekir, sistem durum çubuğunu veya diğer uygulamaları etkilemez - DPI %1$d olarak ayarlandı, uygulama yeniden başlatıldıktan sonra geçerli olur - - Uygulama Dili - Sistemi takip et - Kart Karanlığını Ayarlama - - hata kodu - Lütfen günlük kaydını kontrol edin - Modül yükleniyor %1$d/%2$d - %d yeni modül yüklenemedi - Modül indirme başarısız - Çekirdek Yükleniyor - - Tümü - Root - Özel - Varsayılan - - İsme göre artan sırada - İsme göre azalan sırada - Kurulum zamanı (yeni) - Kurulum zamanı (eski) - Boyuta göre azalan sırada - Boyuta göre artan sırada - Kullanım sıklığına göre - - Bu kategoride uygulama yok - - Yetkilendirme reddedildi - Yetki verildi - Modül Bağlantıları Kaldırılıyor - Modül kaldırma montajını devre dışı bırak - Menüyü genişlet - Menüyü gizle - Üst - Alt - Seçildi - seçenek - - Menü Seçenekleri - Sıralama Seçenekleri - Uygulama Türü Seçimi - - SuSFS Yapılandırması - Yapılandırma Açıklaması - Bu özellik, SuSFS uname değerini ve build time spoofing\'i özelleştirmenize olanak tanır. Ayarlamak istediğiniz değerleri girin ve uygulayın. - Uname Değeri - Lütfen özel uname değeri girin - Derleme Zamanı Sahteciliği - Lütfen build time spoofing değeri girin - Mevcut değer: %s - Mevcut build time: %s - Varsayılana Sıfırla - Uygula - - Sıfırlamayı Onayla - - ksu_susfs dosyası bulunamadı - SuSFS komut çalıştırma başarısız - SuSFS komut çalıştırma hatası: %s - SuSFS uname ve build time başarıyla ayarlandı: %s, %s - - SuSFS Yapılandırması - - Otomatik Başlat - Yeniden başlatma sırasında tüm varsayılan olmayan yapılandırmaları otomatik olarak uygula - Etkinleştirmek için yapılandırma eklenmesi gerekiyor - Otomatik başlatma etkinleştirilemedi - Otomatik başlatma devre dışı bırakılamadı - Otomatik başlatma yapılandırma hatası: %s - Otomatik başlatma için kullanılabilir yapılandırma yok - - Temel Ayarlar - SUS Yolları - SUS Bağlama Noktaları - Bağlamayı Kaldırmayı Dene - Yol Ayarları - Etkinleştirilen Özellikler Durumu - - SUS Yolu Ekle - SUS Bağlama Noktası Ekle - Bağlamayı Kaldırmayı Dene Ekle - SUS yolu başarıyla eklendi - Yol bulunamadı hatası - Yol - Bağlama Yolu - örn.: /system/addon.d - Yapılandırılmış SUS yolu yok - Yapılandırılmış SUS bağlama noktası yok - Yapılandırılmış bağlamayı kaldırmayı dene yok - - Bağlamayı Kaldır Modu - Normal Bağlamayı Kaldır (0) - Ayrı Bağlamayı Kaldır (1) - Normal - Ayrı - Mod: %1$s (%2$s) - Bağlamayı kaldırmayı dene yolu başarıyla eklendi: %s - Bağlamayı kaldırmayı dene yolu kaydetme başarılı: %s - - - SUS Yollarını Sıfırla - Bu, tüm SUS yol yapılandırmalarını temizleyecektir. Devam etmek istiyor musunuz? - SUS Bağlama Noktalarını Sıfırla - Bu, tüm SUS bağlama noktası yapılandırmalarını temizleyecektir. Devam etmek istiyor musunuz? - Bağlamayı Kaldırmayı Dene Sıfırla - Bu, tüm bağlamayı kaldırmayı dene yapılandırmalarını temizleyecektir. Devam etmek istiyor musunuz? - Yol Ayarlarını Sıfırla - - Android Veri Yolu - SD Kart Yolu - Android Veri Yolunu Ayarlayın - SD Kart Yolunu Ayarlayın - - Mevcut SuSFS etkinleştirilen özellikler durumunu göster - Özellik durumu bilgisi bulunamadı - Etkinleştirildi - Devre Dışı Bırakıldı - - SUS Yol Desteği - SUS Bağlama Noktası Desteği - Bağlamayı Kaldırmayı Dene Desteği - Uname Taklit Desteği - Cmdline/Bootconfig Taklit - Açık Yönlendirme Desteği - Günlük Kaydı Desteği - Otomatik Varsayılan Bağlama - Otomatik Bağlama Noktası Bağlama - Otomatik Bağlamayı Kaldırmayı Dene Bağlama - KSU SUSFS Sembollerini Gizle - SUS Kstat Desteği - SUS SU mod değiştirme işlevi - - Yapılandırılabilir SuSFS Özellikleri - SuSFS Günlük Kaydını Etkinleştir - SuSFS için günlük kaydını etkinleştir veya devre dışı bırak - SuSFS Günlük Kaydı Yapılandırması - SuSFS Günlük Kaydı Etkinleştiriliyor - SuSFS günlük kaydını kapat - Güncelleme JSON - Güncelleme JSON URL\'si panoya kopyalandı - - Daha Fazla Modül Bilgisi Göster - Güncelleme JSON URL\'leri gibi ek modül bilgilerini göster - Çalıştırma Konumu - Mevcut çalıştırma konumu: %s - Servis - Post-FS-Data - Sistem servisleri başladıktan sonra çalıştır - Dosya sistemi bağlandıktan sonra ancak sistem tam olarak önyüklenmeden önce çalıştır, önyükleme döngüsüne neden olabilir - Bölüm Bilgisi - Mevcut önyükleme bölümü bilgisini görüntüleyin ve değerleri kopyalayın - Mevcut Aktif Bölüm: %s - Uname: %s - Derleme Zamanı: %s - Mevcut - Uname Kullan - Derleme Zamanı Kullan - Bölüm bilgisi alınamıyor - - SuSFS otomatik başlatma modülü etkinleştirildi, modül yolu: %s - SuSFS otomatik başlatma modülü devre dışı bırakıldı - - Kstat Yapılandırması - Kstat statik yapılandırması eklendi: %1$s - Kstat yapılandırması kaldırıldı: %1$s - Kstat yolu eklendi: %1$s - Kstat yolu kaldırıldı: %1$s - Kstat güncellendi: %1$s - Kstat tam klonu güncellendi: %1$s - Kstat Statik Yapılandırması Ekle - Dosya/Dizin Yolu - İpucu: Orijinal değeri kullanmak için ”default“ kullanabilirsiniz - Kstat Yolu Ekle - Ekle - Kstat Yapılandırmasını Sıfırla - Tüm Kstat yapılandırmalarını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. - Kstat Yapılandırma Açıklaması - • add_sus_kstat_statically: Dosyaların/dizinlerin statik stat bilgisi - • add_sus_kstat: Bind bağlamadan önce yol ekle, orijinal stat bilgisini sakla - • update_sus_kstat: Hedef ino\'yu güncelle, boyut ve blokları değiştirmeden bırak - • update_sus_kstat_full_clone: Sadece ino\'yu güncelle, diğer orijinal değerleri koru - Statik Kstat Yapılandırması - Kstat Yol Yönetimi - Henüz Kstat yapılandırması yok, eklemek için yukarıdaki düğmeye tıklayın - - SUS Bağlama Noktası Gizleme Kontrolü - İşlemler için SUS bağlama noktalarının gizleme davranışını kontrol et - Tüm işlemler için SUS bağlama noktalarını gizle - Etkinleştirildiğinde, SUS bağlama noktaları KSU işlemleri dahil tüm işlemlerden gizlenir - Devre dışı bırakıldığında, SUS bağlama noktaları sadece KSU dışı işlemlerden gizlenir, KSU işlemleri bağlama noktalarını görebilir - Tüm işlemler için SUS bağlama noktalarını gizleme etkinleştirildi - Tüm işlemler için SUS bağlama noktalarını gizleme devre dışı bırakıldı - Ekran kilidi açıldıktan sonra veya service.sh ya da boot-completed.sh aşamasında devre dışı bırakılması önerilir, çünkü bu, KSU işlemi tarafından bağlanan bağlama noktalarına dayanan bazı rootlu uygulamalardaki sorunu çözmelidir - Mevcut ayar: %s - Tüm işlemler için gizle - Sadece KSU dışı işlemler için gizle - Çekirdek Sürümü Özet Modu - SukiSU çekirdek sürümünün gösterdiği sade modu etkinleştirin veya devre dışı bırakın - Android Veri yolu şuna ayarlandı: %s - SD kart yolu şuna ayarlandı: %s - Yol kurulumu tam olarak başarılı olmayabilir, ancak SUS yolları eklenmeye devam edecektir - - Yedekleme - Tüm SuSFS yapılandırmalarının bir yedeğini oluşturun. Yedekleme dosyası tüm ayarları, yolları ve yapılandırmaları içerecektir. - Yedek Oluştur - Yedek başarıyla oluşturuldu: %s - Yedek oluşturma başarısız: %s - Yedekleme dosyası bulunamadı - Geçersiz yedekleme dosyası formatı - Yedekleme sürümü uyuşmuyor, ancak geri yüklemeye çalışılacak - Geri Yükle - SuSFS yapılandırmalarını bir yedekleme dosyasından geri yükleyin. Bu, tüm mevcut ayarların üzerine yazacaktır. - Yedekleme Dosyası Seç - %s tarihinde %s cihazından oluşturulan yedekten yapılandırma başarıyla geri yüklendi - Geri yükleme başarısız: %s - Geri Yüklemeyi Onayla - Bu, tüm mevcut SuSFS yapılandırmalarının üzerine yazacaktır. Devam etmek istediğinizden emin misiniz? - Geri Yükle - Yedekleme Tarihi: %s - Cihaz: %s - Sürüm: %s - Kilitli durum - late_start hizmet modunda önyükleme kilidi durumu özniteliğini geçersiz kıl - Kalıntıları Temizle - Çeşitli modüllerin ve araçların kalıntı dosyalarını ve dizinlerini temizleyin (yanlışlıkla silinerek kayba ve başlatılamamaya neden olabilir, dikkatli kullanın) - SUS Yolunu Düzenle - SUS Bağlama Noktasını Düzenle - Ayırmayı Deneme Yolunu Düzenle - Kstat Statik Yapılandırmasını Düzenle - Kstat Yolunu Düzenle - Kaydet - Düzenle - Sil - Güncelle - Kstat yapılandırması güncellendi - Kstat yolu güncellendi - Susfs tam klon güncellemesi - Zygote İzolasyon Servisi Bağlantısını Kes - Sistem başlangıcında Zygote izolasyon servisi bağlama noktalarının bağlantısını kesmek için bu seçeneği etkinleştirin - Zygote izolasyon servisi bağlantı kesme etkinleştirildi - Zygote izolasyon servisi bağlantı kesme devre dışı bırakıldı - Uygulama Yolu - Diğer yollar - Diğer - Uygulama - Uygulama Yolu Ekle - SuSFS kütüphane sürümü uyuşmazlığı, çekirdek: %1$s vs yönetici: %2$s, Çekirdeği veya yöneticiyi güncellemeniz önerilir - Uyarı - Uygulama Ara - %1$d uygulama seçildi - %1$d uygulama zaten eklendi - Tüm uygulamalar eklendi - Dinamik İmza Yapılandırması - Etkin (Boyut: %s) - Devre Dışı - Dinamik İmzayı Etkinleştir - İmza Boyutu - İmza Hash - Hash, 64 adet onaltılık karakterden oluşmalıdır - Dinamik imza yapılandırması başarıyla ayarlandı - Dinamik imza yapılandırması ayarlanamadı - Geçersiz imza yapılandırması - Dinamik imza devre dışı bırakıldı - Dinamik imza temizlenemedi - Dinamik - İmza %1$d - Bilinmiyor - Aktif Yönetici - Aktif yönetici yok - SukiSU - Zygisk uygulaması - - SUS Döngü Yolları - SUS Döngü Yolu Ekle - SUS Döngü Yolunu Düzenle - SUS döngü yolu başarıyla eklendi: %1$s - SUS döngü yolu kaldırıldı: %1$s - SUS döngü yolu güncellendi: %1$s -> %2$s - Yapılandırılmış SUS döngü yolu yok - Döngü Yollarını Sıfırla - Tüm SUS döngü yollarını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. - Döngü Yolu - /data/ornek/yol - Not: Döngü yolları aracılığıyla yalnızca /storage/ ve /sdcard/ içinde OLMAYAN yollar eklenebilir. - Hata: Döngü yolları /storage/ veya /sdcard/ dizinleri içinde olamaz - Döngü Yolları - Döngü Yolu Ekle - - Döngü Yolu Yapılandırması - Döngü yolları, her kök olmayan (non-root) kullanıcı uygulaması veya yalıtılmış hizmet başlangıcında SUS_PATH olarak yeniden işaretlenir. Bu, eklenen yolların inode durumunun sıfırlanması veya çekirdekte yeniden oluşturulması gibi sorunları gidermeye yardımcı olur. - AVC Günlük Kaydı Taklidi - AVC günlük kaydı taklidi etkinleştirildi - AVC günlük kaydı taklidi devre dışı bırakıldı - devre dışı: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'in taklit edilmesini devre dışı bırakır.\n -etkin: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'i \'kernel\' olarak taklit etmeyi etkinleştirir. - Önemli Not:\n -- Çekirdekte varsayılan olarak \'0\' değerine ayarlıdır.\n -- Bu özelliği etkinleştirmek, geliştiricilerin bir izin veya SELinux sorunu için hata ayıklaması yaparken sorunun kaynağını bulmalarını zorlaştırabilir. Bu nedenle, bu tür işlemler sırasında özelliğin devre dışı bırakılması tavsiye edilir. - - Doğrulandı - Modül imzası doğrulandı - İmza Doğrulaması - Modüller yüklendiğinde imza doğrulamasını zorunlu kıl. (Sadece ARM mimarisi için geçerlidir) - Bilinmeyen yayıncı - İmzasız modüller eksik veya değiştirilmiş olabilir. Cihazınızı korumak için bu modülün kurulumu engellenmiştir. - İmzasız modüller eksik veya değiştirilmiş olabilir. Bilinmeyen bir yayıncıdan gelen aşağıdaki modülün bu cihaza kurulmasına izin vermek istiyor musunuz? - Hook tipi - - KPM Yaması - Ek KPM özellikleri eklemek için - KPM Yaması - Yüklemeden önce çekirdek imajına KPM yamasını uygula - KPM Yamasını Geri Al - Daha önce uygulanan KPM yamasını geri al - KPM yaması etkinleştirildi - KPM yaması geri alma etkinleştirildi - KPM Yama Modu - KPM Yamasını Geri Alma Modu - - KPM araçları hazırlanıyor - KPM yaması uygulanıyor - KPM yaması geri alınıyor - İmaj dosyası bulundu: %s - KPM yaması başarıyla uygulandı - KPM yaması başarıyla geri alındı - Dosya başarıyla yeniden paketlendi - - Zip dosyası çıkarılamadı - İmaj dosyası bulunamadı - KPM yaması başarısız oldu - KPM yamasını geri alma başarısız oldu - KPM yama işlemi başarısız oldu: %s - - Çekirdeği Takip Et - Çekirdeği herhangi bir KPM değişikliği olmadan olduğu gibi kullan - - Kullanıcı modu uygulama listesi taraması - Bu seçeneği etkinleştirmek, uygulama listesi için kullanıcı modu taramasını kullanarak kararlılığı artıracaktır. (Uygulama listesinin çekirdek tarafından taranması sırasında donma gibi sorunlar yaşıyorsanız, bu seçeneği etkinleştirmeyi deneyebilirsiniz.) - Çok Kullanıcılı Uygulama Taraması - Etkinleştirildiğinde, iş profilleri de dahil olmak üzere tüm kullanıcıların uygulamalarını tarar. - Ayar başarısız oldu, lütfen izinleri kontrol edin - Çalışma Zamanı Ortamını Temizle - Çalışma zamanı dosyalarını temizleyin ve tarayıcı hizmetini durdurun - Çalışma zamanı ortamını temizlemek istediğinizden emin misiniz? Bu işlem tarayıcı hizmetini durduracak ve ilgili dosyaları kaldıracaktır. - Çalışma zamanı ortamı başarıyla temizlendi - Çalışma zamanı ortamı temizlenemedi - - Kurulumu Onayla - Kurulumu Onayla (%d dosya) - Yükle - Modül - Kernel - Bilinmiyor - Bilinmeyen Kernel - Bilinmeyen Dosya - Sürüm - Geliştirici - Açıklama - Desteklenen Cihazlar - - SUS Eşlemeleri - Kütüphane Yolu - /data/adb/modules/my_module/zygisk/arm64-v8a.so - SUS Eşlemesi Ekle - SUS Eşlemesini Düzenle - SUS eşlemesi başarıyla eklendi: %1$s - SUS eşlemesi kaldırıldı: %1$s - SUS eşlemesi güncellendi: %1$s -> %2$s - Yapılandırılmış SUS eşlemesi yok - SUS Eşlemelerini Sıfırla - Bu, yapılandırılmış tüm SUS eşlemelerini kaldıracaktır. Bu işlem geri alınamaz. - Bellek Eşlemesi Gizleme - /proc/self/ içindeki çeşitli eşlemelerden mmap\'lenmiş gerçek dosyayı gizle - Bellek eşlemelerinin gerçek dosya yollarını /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] konumundan gizleyin. Lütfen dikkat: Bu özellik, anonim bellek eşlemelerini gizlemeyi desteklemez ve enjekte edilen kütüphanenin kendisinin neden olduğu satır içi kancaları veya PLT kancalarını da gizleyemez. - Önemli Not: İyi uygulanmış enjeksiyon tespit mekanizmalarına sahip uygulamalar için bu özellik, tespiti etkili bir şekilde atlatamayabilir. - Öncelikle, ps -enf kullanarak hedef uygulamanın PID ve UID\'sini bulun, ardından /proc/<pid>/maps içindeki ilgili yolları kontrol edin ve tutarlılığı sağlamak için cihaz numaralarını /proc/1/mountinfo\'dakilerle karşılaştırın. Yalnızca cihaz numaraları eşleştiğinde harita gizleme işlevi düzgün çalışabilir. - - Günlük Görüntüleyici - Geri - Ara - Günlükleri Temizle - Seçili günlük dosyasını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. - Günlükler başarıyla temizlendi - Türe Göre Filtrele - Tüm Türler - %2$d girişten %1$d tanesi gösteriliyor - Günlük bulunamadı - Eşleşen günlük bulunamadı - Yenile - Ham Günlük - UID, komut veya ayrıntılara göre ara… - Aramayı temizle - Kullanım Günlüklerini Görüntüle - KernelSU süper kullanıcı erişim günlüklerini görüntüle - Alt türleri hariç tut - Mevcut Uygulama - Sayfa: %1$d/%2$d | Toplam günlük: %3$d - Çok fazla günlük var, yalnızca son %1$d giriş gösteriliyor - Daha Fazla Günlük Yükle - Tüm günlükler görüntülendi - - SukiSU Yöneticisi Kaldırılsın mı? - Kaldırma işlemine devam etmek, root erişiminizin temel işlevselliğini etkilemeyecektir. Root, bu yöneticiden bağımsız olarak çalışacak şekilde tasarlanmıştır. - Mevcut yönetici bu çekirdekle uyumsuz! Lütfen çekirdeği %2$d veya daha yüksek bir sürüme yükseltin (mevcut sürüm %1$d) diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index e4071105..d603f92a 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -2,14 +2,16 @@ Головна Не встановлено - Натисніть, щоб встановити + Натисніть щоб встановити Працює - Версія: %s + Версія: %d + Суперкористувачі: %d + Модулі: %d Не підтримується - Драйвер KernelSU не виявлено у вашому ядрі. Можливо, у вас неправильне ядро. + KernelSU зараз підтримує лише ядра GKI. Версія ядра - Версія SuSFS Версія менеджера + Відбиток Статус SELinux Вимкнено Примусовий @@ -18,515 +20,138 @@ Суперкористувач Не вдалося ввімкнути модуль: %s Не вдалося вимкнути модуль: %s - Немає встановлених модулів + Модуль не встановлено Модулі - Сортувати (Спочатку дії) - Сортувати (Спочатку ввімкнені) Видалити Встановити Встановити Перезавантажити Налаштування М\'яке перезавантаження - Перезавантажити в Recovery - Перезавантажити в Bootloader - Перезавантажити в режим Download - Перезавантажити в режим EDL + Перезавантажити до Recovery + Перезавантажити до Bootloader + Перезавантажити до Download + Перезавантажити до EDL Про додаток Ви впевнені, що хочете видалити модуль %s? - %s видалено + %s невстановлено Не вдалося видалити: %s Версія Автор - Оновити + Модулі недоступні, оскільки OverlayFS вимкнено ядром! + Освіжати(Оновити) Показати системні додатки Сховати системні додатки - Надіслати логи + Надіслати журнали Безпечний режим Перезавантажте, щоб застосувати Модулі недоступні через конфлікт з Magisk! - Дізнатися про KernelSU + Дізнайтеся про KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Дізнайтеся, як встановити KernelSU та використовувати модулі + Дізнайтеся, як інсталювати KernelSU і використовувати модулі. Підтримати нас - KernelSU є, і завжди буде, безкоштовним та з відкритим вихідним кодом. Однак ви можете показати нам свою підтримку, зробивши пожертву. + KernelSU є і завжди буде безкоштовним програмним забезпеченням з відкритим вихідним кодом. Однак ви можете показати нам, що вам небайдужа наша допомога, зробивши пожертву. Приєднуйтесь до нашого каналу %2$s]]> - Профіль додатку - За замовчуванням + Профіль додатка + Типовий Шаблон Власний Назва профілю + Змонтувати простір імен + Наслідуваний + Глобальний + Індивідуальний Групи Можливості Контекст SELinux - Відмонтувати модулі - Не вдалося оновити профіль додатку для %s - Поточна версія KernelSU %s занадто низька для коректної роботи менеджера. Будь ласка, оновіться до версії %s або вище! - Відмонтовувати модулі за замовчуванням - Глобальне значення за замовчуванням для "Відмонтувати модулі" у профілі додатку. Якщо ввімкнено, це видалить усі зміни системи, зроблені модулями, для додатків без встановленого профілю. - Увімкнення цієї опції дозволить KernelSU відновити будь-які змінені модулями файли для цього додатку. + Розмонтувати модулі + Не вдалося оновити профіль додатка для %s + Розмонтування модулів + Загальне значення за замовчуванням для \"Розмонтувати модулі\" у профілях додатків. Якщо ввімкнено, буде видалено всі модифікації модулів у системі для додатків, які не мають встановленого профілю. + Увімкнення цієї опції дозволить KernelSU відновити будь-які змінені файли модулями для цієї програми. Домен Правила Оновити Завантаження модуля: %s Початок завантаження: %s - Доступна нова версія %s, натисніть для оновлення. Запустити - Примусово зупинити + Примусова зупинка Перезапустити + Доступна нова версія %s, натисніть, щоб оновити! Не вдалося оновити правила SELinux для %s - Список змін - Шаблон профілю додатку - Керування локальними та онлайн-шаблонами профілю додатку + Журнал змін + Поточна версія KernelSU %d занадто низька для належної роботи менеджера. Будь ласка, оновіть його до версії %d або вище! + Успішно імпортовано + Експортувати в буфер обміну + Не вдається знайти локальний шаблон для експорту! + Ідентифікатор шаблону вже існує! + Імпортувати з буферу обміну + Невдача при завантаженні списку змін: %s + Ім\'я + Недійсний ідентифікатор шаблону + Синхронізувати мережеві шаблони Створити шаблон + Тільки для читання + Імпорт/Експорт + Помилка при збереженні шаблону Редагувати шаблон - ID - Недійсний ID шаблону - Назва + Ідентифікатор + Шаблон профілю програми Опис Зберегти + Керувати локальними та мережевими шаблонами профілів додатків. Видалити + Буфер обміну пустий! Переглянути шаблон - Тільки для читання - ID шаблону вже існує! - Імпорт/Експорт - Імпортувати з буфера обміну - Експортувати в буфер обміну - Не знайдено локальних шаблонів для експорту! - Імпортовано успішно - Синхронізувати онлайн-шаблони - Не вдалося зберегти шаблон - Буфер обміну порожній! - Не вдалося завантажити список змін: %s - Перевіряти оновлення - Автоматично перевіряти оновлення при відкритті додатку - Не вдалося надати root-права! - Дія - Закрити - Увімкнути налагодження WebView - Можна використовувати для налагодження WebUI. Будь ласка, вмикайте лише за потреби. - Пряме встановлення (рекомендовано) - Виберіть образ для патчу - Встановити в неактивний слот (після OTA) - Ваш пристрій буде **ПРИМУСОВО** завантажено в поточний неактивний слот після перезавантаження!\nВикористовуйте цю опцію лише після завершення OTA.\nПродовжити? + Налагодження WebView + Виберіть KMI Далі - Рекомендується образ розділу %1$s - Вибрати KMI - Видалити + Перевірити наявність оновлень + Автоматична перевірка оновлень під час відкриття програми. + Можна використовувати для налагодження веб-інтерфейсу. Вмикайте лише за потреби. + Пряме встановлення (рекомендовано) + Виберіть файл + Встановити в неактивний слот (після OTA) + Ваш пристрій буде **ПРИМУСОВО** завантажено в поточний неактивний слот після перезавантаження! +\n Використовуйте цю опцію тільки після завершення OTA. +\n Продовжити? + %1$s образ розділу рекомендується + Не вдалося отримати root! + Відкрити Тимчасово видалити Видалити назавжди - Відновити стоковий образ - Тимчасово видалити KernelSU, відновлення до початкового стану після наступного перезавантаження. - Повне та остаточне видалення KernelSU (Root та всі модулі). - Відновити стоковий заводський образ (якщо існує резервна копія), зазвичай використовується перед OTA; якщо потрібно видалити KernelSU, використовуйте "Видалити назавжди". + Відновити стокове зображення + Тимчасово видалити KernelSU, відновити початковий стан після наступного перезавантаження. + Видалити Прошивка - Прошивка успішна - Прошивка не вдалася - Обраний LKM: %s - Зберегти логи - Логи збережено - - Підтвердити встановлення модуля %1$s? - невідомий модуль - - Підтвердити відновлення модулів - Ця операція перезапише всі існуючі модулі. Продовжити? + Прошивку виконано + Прошивка не виконана + Вибраний ЛКМ: %s + Повне та остаточне видалення KernelSU (root та всіх модулів). + Відновити стоковий заводський образ (якщо є резервна копія), зазвичай використовується перед OTA; якщо вам потрібно видалити KernelSU, використовуйте \"Назавжди видалити\". + Зберегти Журнали + Будуть встановлені такі модулі: %1$s + Сортувати (спочатку за дією) + Сортувати (спочатку ввімкнено) Підтвердити - Скасувати - - Резервне копіювання успішне (tar.gz) - Не вдалося створити резервну копію: %1$s - резервне копіювання модулів - відновлення модулів - - Модулі успішно відновлено, потрібне перезавантаження - Не вдалося відновити: %1$s - Перезавантажити зараз - Невідома помилка - - Не вдалося виконати команду: %1$s - - Резервне копіювання білого списку успішне - Не вдалося створити резервну копію білого списку: %1$s - Підтвердити відновлення білого списку - Ця операція перезапише поточний білий список. Продовжити? - Білий список успішно відновлено - Не вдалося відновити білий список: %1$s - Резервна копія білого списку - Відновити білий список - Власний фон додатку - Вибрати зображення як фон - Прозорість панелі навігації - Версія Android - Модель пристрою - Надання прав суперкористувача для %s заборонено - Вимкнути сумісність з su - Тимчасово заборонити будь-яким додаткам отримувати root-права через команду su (існуючі root-процеси не будуть зачеплені). - Ви впевнені, що хочете встановити наступні %1$d модулі? \n\n%2$s - Більше налаштувань - SELinux - Увімкнено - Вимкнено - Спрощений режим - Приховує непотрібні картки при ввімкненні - Приховати версію ядра - Приховує версію ядра - Приховати іншу інформацію - Приховує червону крапку про кількість суперкористувачів, модулів та KPM-модулів на сторінці навігаційної панелі - Приховати статус SuSFS - Приховує інформацію про статус SuSFS на головній сторінці - Приховати картку посилань - Приховує інформацію на картці посилань на головній сторінці - Приховати рядки тегів модуля - Приховує мітки з назвою папки та розміром у картках модулів - Тема - Як у системі - Світла - Темна - Ручний хук - Динамічні кольори - Динамічні кольори з використанням системних тем - Вибрати колір теми - Синій - Зелений - Фіолетовий - Помаранчевий - Рожевий - Сірий - Жовтий - Встановити Anykernel3 - Прошити файл ядра AnyKernel3 - Потрібні права суперкористувача - Очищення завершено - Перезавантажитися негайно? - Так - Ні - Не вдалося перезавантажити - KPM - На даний момент немає встановлених модулів ядра - Версія - Автор - Видалити - Видалено успішно - Не вдалося видалити - Завантаження модуля kpm успішне - Не вдалося завантажити модуль kpm - Параметри - Виконати - Версія KPM - Закрити - Наступні функції модулів ядра були розроблені KernelPatch та модифіковані для включення функцій модулів ядра SukiSU Ultra - SukiSU Ultra з нетерпінням чекає - Успішно - Невдача - У майбутньому SukiSU Ultra буде відносно незалежною гілкою KSU, але ми все ще вдячні офіційному KernelSU, MKSU та іншим за їхній внесок! - Не підтримується - Підтримується - Ядро не пропатчене - Ядро не налаштоване - Власні налаштування - Встановлення KPM - Завантажити - Вбудувати - Будь ласка, оберіть: %1$s режим встановлення модуля \n\nЗавантажити: Тимчасово завантажити модуль \nВбудувати: Постійно встановити в систему - Не вдалося перевірити існування файлу модуля - Колір теми - Неправильний тип файлу! Будь ласка, виберіть файл .kpm. - Видалити - Буде видалено наступний KPM: %s - Використовуйте два пальці для масштабування зображення та один палець для перетягування, щоб налаштувати положення - Переналаштувати - - Прошивка завершена - - Підготовка… - Очищення файлів… - Копіювання файлів… - Розпакування інструменту прошивки… - Патчинг скрипту прошивки… - Прошивка ядра… - Прошивка завершена - - Вибрати слот для прошивки - Будь ласка, виберіть цільовий слот для прошивки boot - Слот A - Слот B - Вибраний слот: %1$s - Отримання оригінального слоту - Встановлення вказаного слоту - Відновити слот за замовчуванням - Поточний системний слот за замовчуванням: %1$s - - Не вдалося скопіювати - Невідома помилка - Прошивка не вдалася - - Відновлення/встановлення LKM - Прошивка AnyKernel3 - Версія ядра: %1$s - Використовується інструмент для патчингу: %1$s - Налаштувати - Налаштування додатку - Інструменти - - Додаток не знайдено - SELinux увімкнено - SELinux вимкнено - Не вдалося змінити статус SELinux - Розширені налаштування - Налаштувати панель інструментів - Повернутися - Фон успішно встановлено - Видалено власні фони - Альтернативна іконка - Змінити іконку запуску на іконку KernelSU. - Іконку змінено - - Приховати функцію KPM - Приховує інформацію та функцію KPM на головному екрані та в нижній панелі - - Виберіть рушій WebUI для використання - Автоматичний вибір - Примусово використовувати WebUI X - Примусово використовувати KSU WebUI - Впровадити Eruda у WebUI X - Впровадити консоль налагодження у WebUI X для полегшення налагодження. Потребує увімкненого веб-налагодження. - - Застосований DPI - Налаштувати щільність відображення екрана лише для поточного додатку - Маленький - Середній - Великий - Надвеликий - Власний - Застосування налаштувань DPI - Підтвердити зміну DPI - Ви впевнені, що хочете змінити DPI додатку з %1$d на %2$d? - Додаток потрібно перезапустити, щоб застосувати нові налаштування DPI; це не вплине на системний рядок стану або інші додатки - DPI встановлено на %1$d, набуде чинності після перезапуску додатку - - Мова додатку - Як у системі - Налаштування затемнення карток - - код помилки - Будь ласка, перевірте лог - Встановлення модуля %1$d/%2$d - Не вдалося встановити %d новий модуль - Не вдалося завантажити модуль - Прошивка ядра - - Всі - Root - Власні - За замовчуванням - - Назва за зростанням - Назва за спаданням - Час встановлення (нові) - Час встановлення (старі) - Розмір за спаданням - Розмір за зростанням - Частота використання - - Немає додатків у цій категорії - - Заборонити права - Надати права - Відмонтувати монтування модулів - Вимкнути відмонтування модулів - Розгорнути меню - Згорнути меню - Вгору - Вниз - Вибрано - Вибрати - - Опції меню - Сортувати за - Вибір типу додатку - - Налаштування SuSFS - Опис налаштувань - Ця функція дозволяє налаштовувати підміну значення uname та часу збірки SuSFS. Введіть потрібні значення та натисніть "Застосувати", щоб вони набули чинності. - Значення Uname - Будь ласка, введіть власне значення uname - Підміна часу збірки - Будь ласка, введіть значення для підміни часу збірки - Поточне значення: %s - Поточний час збірки: %s - Скинути до замовчування - Застосувати - - Підтвердити скидання - - Не вдалося знайти файл ksu_susfs - Не вдалося виконати команду SuSFS - Помилка виконання команди SuSFS: %s - Значення uname та час збірки SuSFS успішно встановлено: %s, %s - - Налаштування SuSFS - - Автозапуск - Автоматично застосовувати всі нестандартні конфігурації при перезавантаженні - Для ввімкнення потрібно додати конфігурацію - Не вдалося ввімкнути автозапуск - Не вдалося вимкнути автозапуск - Помилка конфігурації автозапуску: %s - Немає доступної конфігурації для автозапуску - - Основні налаштування - Шляхи SUS - Монтування SUS - Спроба відмонтування - Налаштування шляхів - Статус увімкнених функцій - - Додати шлях SUS - Додати монтування SUS - Додати спробу відмонтування - Шлях SUS успішно додано - Помилка: шлях не знайдено - Шлях - Шлях монтування - напр.: /system/addon.d - Немає налаштованих шляхів SUS - Немає налаштованих монтувань SUS - Немає налаштованих спроб відмонтування - - Режим відмонтування - Звичайне відмонтування (0) - Від\'єднане відмонтування (1) - Звичайний - Від\'єднаний - Режим: %1$s (%2$s) - Шлях для спроби відмонтування успішно додано: %s - Спроба збереження шляху відмонтування успішна: %s - - - Скинути шляхи SUS - Це видалить усі конфігурації шляхів SUS. Ви впевнені, що хочете продовжити? - Скинути монтування SUS - Це видалить усі конфігурації монтувань SUS. Ви впевнені, що хочете продовжити? - Скинути спроби відмонтування - Це видалить усі конфігурації спроб відмонтування. Ви впевнені, що хочете продовжити? - Скинути налаштування шляхів - - Шлях до Android Data - Шлях до SD-карти - Встановити шлях до Android Data - Встановити шлях до SD-карти - - Відображення поточного статусу увімкнених функцій SuSFS - Інформацію про статус функцій не знайдено - Увімкнено - Вимкнено - - Підтримка шляхів SUS - Підтримка монтувань SUS - Підтримка спроб відмонтування - Підтримка підміни uname - Підміна Cmdline/Bootconfig - Підтримка Open Redirect - Підтримка логування - Автоматичне монтування за замовчуванням - Автоматичне прив\'язане монтування - Автоматична спроба відмонтування прив\'язаного монтування - Приховати символи KSU SUSFS - Підтримка SUS Kstat - Функція перемикання режиму SUS SU - - Настроювані функції SuSFS - Увімкнути лог SuSFS - Увімкнути або вимкнути логування для SuSFS - Налаштування логування SuSFS - Увімкнення логування SuSFS - Вимкнення логування SuSFS - Оновити JSON - URL для оновлення JSON скопійовано в буфер обміну - - Показувати більше інформації про модуль - Відображати додаткову інформацію про модуль, як-от URL-адреси для оновлення JSON - Місце виконання - Поточне місце виконання: %s - Сервіс - Post-FS-Data - Виконувати після запуску системних сервісів - Виконувати після монтування файлової системи, але до повного завантаження системи. Може спричинити бутлуп - Інформація про слот - Переглянути інформацію про поточний завантажувальний слот та скопіювати значення - Поточний активний слот: %s - Uname: %s - Час збірки: %s - Поточний - Використовувати Uname - Використовувати час збірки - Не вдалося отримати інформацію про слот - - Модуль автозапуску SuSFS увімкнено, шлях до модуля: %s - Модуль автозапуску SuSFS вимкнено - - Налаштування Kstat - Статичну конфігурацію Kstat додано: %1$s - Конфігурацію Kstat видалено: %1$s - Шлях Kstat додано: %1$s - Шлях Kstat видалено: %1$s - Kstat оновлено: %1$s - Повне клонування Kstat оновлено: %1$s - Додати статичну конфігурацію Kstat - Шлях до файлу/каталогу - Підказка: Ви можете використовувати "default" для використання оригінального значення - Додати шлях Kstat - Додати - Скинути конфігурацію Kstat - Ви впевнені, що хочете очистити всі конфігурації Kstat? Цю дію неможливо скасувати. - Опис конфігурації Kstat - • add_sus_kstat_statically: Статична інформація про файли/каталоги - • add_sus_kstat: Додати шлях перед прив\'язаним монтуванням, зберігаючи оригінальну інформацію - • update_sus_kstat: Оновити цільовий ino, залишивши розмір та блоки без змін - • update_sus_kstat_full_clone: Оновити лише ino, залишивши інші оригінальні значення - Статична конфігурація Kstat - Керування шляхами Kstat - Конфігурації Kstat ще немає, натисніть кнопку вище, щоб додати - - Керування приховуванням монтувань SUS - Керуйте поведінкою приховування монтувань SUS для процесів - Приховувати монтування SUS для всіх процесів - Якщо увімкнено, монтування SUS будуть приховані від усіх процесів, включно з процесами KSU - Якщо вимкнено, монтування SUS будуть приховані лише від процесів, що не належать KSU; процеси KSU зможуть бачити монтування - Увімкнено приховування монтувань SUS для всіх процесів - Вимкнено приховування монтувань SUS для всіх процесів - Рекомендується встановлювати у вимкнений стан після розблокування екрана, або на етапі service.sh чи boot-completed.sh, оскільки це повинно виправити проблему з деякими рутованими додатками, які залежать від монтувань, створених процесом KSU - Поточне налаштування: %s - Приховувати для всіх процесів - Приховувати лише для процесів, що не належать KSU - Спрощений режим версії ядра - Увімкнути або вимкнути спрощене відображення версії ядра SukiSU - Шлях до Android Data встановлено на: %s - Шлях до SD-карти встановлено на: %s - Налаштування шляху може бути не повністю успішним, але шляхи SUS будуть додаватися - - Резервне копіювання - Створити резервну копію всіх конфігурацій SuSFS. Файл резервної копії включатиме всі налаштування, шляхи та конфігурації. - Створити резервну копію - Резервну копію створено успішно: %s - Не вдалося створити резервну копію: %s - Файл резервної копії не знайдено - Недійсний формат файлу резервної копії - Невідповідність версії резервної копії, але буде зроблена спроба відновлення - Відновлення - Відновити конфігурації SuSFS з файлу резервної копії. Це перезапише всі поточні налаштування. - Вибрати файл резервної копії - Конфігурацію успішно відновлено з резервної копії, створеної %s на пристрої: %s - Не вдалося відновити: %s - Підтвердити відновлення - Це перезапише всі поточні конфігурації SuSFS. Ви впевнені, що хочете продовжити? - Відновити - Дата резервної копії: %s - Пристрій: %s - Версія: %s - Заблокированное состояние - Переопределить свойство состояния блокировки загрузчика в режиме службы late_start - Очистити залишки - Очищення залишкових файлів та каталогів різних модулів та інструментів (може призвести до випадкового видалення, втрати даних та неможливості завантаження, використовуйте з обережністю) + Не вдалося надати доступ суперкористувачу для %s + Дія + Журнали збережено + Вимкнути сумісність із su + Вимкніть можливість будь-якої програми отримувати root-права за допомогою команди ⁠su (існуючі root-процеси залишаться в силі). + Перевірити наявність оновлень модулів + Використовувати локальний LKM-файл + Підтримуються лише файли .ko + Вимкнути розмонтування ядра + Вимкнути поведінку розмонтування на рівні ядра, контрольовану KernelSU. + Увімкнути посилену безпеку + Увімкніть суворіші політики безпеки. + За замовчуванням + Тимчасово ввімкнути + Увімкнути назавжди + Обробка… + Потягніть униз, щоб оновити + Відпустіть для оновлення + Оновлення… + Оновлено успішно diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index b62325b1..bd8da6ee 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -1,27 +1,47 @@ + Hồ sơ ứng dụng + Mặc định + Bản mẫu + Tuỳ chỉnh + Tên hồ sơ + Nhóm + Cập nhật Hồ sơ ứng dụng cho %s thất bại + Umount modules + Giá trị mặc định chung cho \"Umount modules\" trong Hồ sơ ứng dụng. Nếu được bật, mọi thay đổi hệ thống do các module gây ra sẽ bị gỡ bỏ khỏi hệ thống và các ứng dụng chưa thiết lập hồ sơ. + Bật tùy chọn này sẽ cho phép KernelSU khôi phục mọi file đã được các module sửa đổi trong ứng dụng này. + Cập nhật + Đang tải xuống module: %s + Bắt đầu tải xuống: %s + Phiên bản mới %s đã có sẵn, nhấn để cập nhật! + Tìm hiểu về KernelSU + Tìm hiểu cách cài đặt KernelSU và sử dụng các module. + Ủng hộ chúng tôi + KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp! + Tham gia kênh %2$s của chúng tôi]]> + Các module bị vô hiệu hoá do xung đột với Magisk! + Bạn có THẬT SỰ muốn gỡ cài đặt module %s không? + Gửi nhật ký Trang chủ Chưa cài đặt Nhấn để cài đặt Đang hoạt động - Phiên bản: %s + Phiên bản: %d Không được hỗ trợ - Không phát hiện được Trình điều khiển SukiSU Ultra trên Kernel của bạn, Kernel sai? + KernelSU hiện tại chỉ hỗ trợ Kernel GKI. Phiên bản Kernel - Phiên bản SuSFS - Phiên bản Trình quản lý + Phiên bản trình quản lý + Fingerprint Trạng thái SELinux Vô hiệu hoá Enforcing Permissive - Không xác định + Không rõ Superuser Không thể kích hoạt module: %s Không thể vô hiệu hoá module: %s - Chưa cài đặt module nào + Chưa có module nào được cài đặt Module - Sắp xếp (Theo hành động) - Sắp xếp (Theo trạng thái) Gỡ cài đặt Cài đặt Cài đặt @@ -30,682 +50,106 @@ Khởi động lại mềm Khởi động lại vào Recovery Khởi động lại vào Bootloader - Khởi động lại vào Download Mode + Khởi động lại vào Download Khởi động lại vào EDL Thông tin - Bạn có THẬT SỰ muốn gỡ cài đặt module %s không? %s đã được gỡ cài đặt Gỡ cài đặt thất bại: %s Phiên bản Tác giả + Các module không khả dụng vì OverlayFS đã bị vô hiệu hoá bởi Kernel! Làm mới - Hiển thị ứng dụng hệ thống - Ẩn ứng dụng hệ thống - Gửi nhật ký + Hiển thị các ứng dụng hệ thống + Ẩn các ứng dụng hệ thống Chế độ an toàn Khởi động lại để có hiệu lực - Các module không khả dụng do xung đột với Magisk! - Tìm hiểu về KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Tìm hiểu cách cài đặt KernelSU và sử dụng các module! - Ủng hộ chúng tôi - KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp! - Tham gia kênh %2$s của chúng tôi

Hình ảnh của các tệp có nhãn dán nhân vật anime thuộc bản quyền của %3$s, Quyền sở hữu trí tuệ thương hiệu trong hình ảnh thuộc về %4$s. Trước khi sử dụng các tệp này, ngoài việc tuân thủ %5$s, bạn cũng cần tuân thủ sự cho phép của hai tác giả để sử dụng các nội dung nghệ thuật này]]>
- Mặc định - Bản mẫu - Tuỳ chỉnh - Tên hồ sơ - Nhóm - Tính tương thích - Bối cảnh SELinux - Umount modules - Cập nhật Hồ sơ ứng dụng cho %s thất bại - Phiên bản SukiSU Ultra hiện tại %s quá thấp để Trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %s hoặc cao hơn! - Umount modules - Giá trị mặc định chung cho \"Umount modules\" trong Hồ sơ ứng dụng. Nếu được bật, mọi thay đổi hệ thống do các module gây ra sẽ bị gỡ bỏ khỏi hệ thống và các ứng dụng chưa thiết lập hồ sơ - Bật tùy chọn này sẽ cho phép SukiSU Ultra khôi phục mọi file đã được các module sửa đổi trong ứng dụng này + Superusers: %d + Modules: %d Tên miền Quy tắc - Cập nhật - Đang tải xuống module: %s - Bắt đầu tải xuống: %s - Phiên bản mới %s đã có sẵn, nhấn để cập nhật - Mở - Buộc dừng + Khởi chạy Khởi động lại + Không gian tên + Tính tương thích Cập nhật quy tắc SELinux cho %s thất bại - Changelog - Mẫu Hồ sơ ứng dụng - Quản lý mẫu cục bộ và trực tuyến của Hồ sơ ứng dụng - Tạo mẫu - Chỉnh sửa mẫu - ID - ID mẫu không hợp lệ + Buộc dừng + Thừa hưởng + Toàn cục + Riêng biệt + Bối cảnh SELinux + Umount modules + Phiên bản KernelSU hiện tại %d quá thấp để trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %d hoặc cao hơn! + Đã nhập thành công + Xuất vào bộ nhớ tạm + Không tìm thấy mẫu cục bộ để xuất! + ID mẫu đã tồn tại! + Nhật ký thay đổi + Nhập từ bộ nhớ tạm + Lấy nhật ký thay đổi thất bại: %s Tên + ID mẫu không hợp lệ + Đồng bộ hoá các mẫu trực tuyến + Tạo mẫu + Nhập/Xuất + Lưu mẫu thất bại + Chỉnh sửa mẫu + Mẫu Hồ sơ ứng dụng Mô tả Lưu - Xoá + Quản lý mẫu cục bộ và trực tuyến của Hồ sơ ứng dụng. + Xóa + Bộ nhớ tạm đang trống! Xem mẫu Chỉ đọc - ID mẫu đã tồn tại! - Nhập/Xuất - Nhập từ bộ nhớ tạm clipboard - Xuất vào bộ nhớ tạm clipboard - Không tìm thấy mẫu cục bộ để xuất! - Đã nhập thành công - Đồng bộ hoá các mẫu trực tuyến - Lưu mẫu thất bại - Bộ nhớ tạm đang trống! - Lấy changelog thất bại: %s - Kiểm tra cập nhật - Tự động kiểm tra cập nhật khi mở ứng dụng - Cấp quyền root thất bại! - Khởi chạy - Đóng + ID Gỡ lỗi WebView - Có thể sử dụng để gỡ lỗi WebUI. Vui lòng chỉ bật khi cần thiết - Cài đặt trực tiếp (Khuyến nghị) - Chọn file .img cần vá - Cài đặt vào phân vùng chưa được sử dụng (Sau OTA) - Thiết bị của bạn sẽ **BUỘC** phải khởi động vào phân vùng chưa được sử dụng!\nChỉ sử dụng tùy chọn này sau khi cập nhật OTA hoàn tất.\nTiếp tục? - Kế tiếp - Phân vùng %1$s được khuyến nghị + Có thể sử dụng để gỡ lỗi WebUI. Vui lòng chỉ bật khi cần thiết. + Cấp quyền root thất bại! + Kiểm tra cập nhật + Tự động kiểm tra cập nhật khi mở ứng dụng. + Mở + Cài đặt vào phân vùng không hoạt động (Sau OTA) + Thiết bị của bạn sẽ **BUỘC** phải khởi động vào phân vùng không hoạt động hiện tại sau khi khởi động lại!\nChỉ dùng tùy chọn này khi cập nhật OTA đã hoàn tất!\nTiếp tục? + Gỡ cài đặt tạm thời KernelSU, khôi phục lại trạng thái ban đầu sau lần khởi động lại tiếp theo. Chọn KMI + Kế tiếp + Cài đặt trực tiếp (Khuyến nghị) + Chọn file Gỡ cài đặt Gỡ cài đặt tạm thời - Gỡ cài đặt sạch - Khôi phục phân vùng khởi động về mặc định - Gỡ cài đặt tạm thời SukiSU Ultra, khôi phục lại trạng thái ban đầu sau lần khởi động lại tiếp theo - Gỡ cài đặt SukiSU Ultra (Root và tất cả các module) sạch hoàn toàn, trả về trạng thái ban đầu - Khôi phục lại boot lúc đầu (Nếu có bản sao lưu), thường được sử dụng trước OTA; nếu bạn cần gỡ hẳn SukiSU Ultra, sử dụng \"Gỡ cài đặt sạch\" + Gỡ cài đặt vĩnh viễn + Khôi phục image gốc + Gỡ cài đặt KernelSU (root và tất cả các module) sạch hoàn toàn, trả về trạng thái ban đầu. + Khôi phục lại image gốc (nếu có bản sao lưu), thường được sử dụng trước OTA; nếu bạn cần gỡ hẳn KernelSU, hãy sử dụng \"Gỡ cài đặt vĩnh viễn\". Đang Flash... Flash thành công Flash thất bại - Đã chọn LKM: %s + LKM đã chọn: %s + Phân vùng image %1$s được khuyến nghị Lưu nhật ký - Nhật ký đã được lưu - - Xác nhận cài đặt module %1$s? - Module không xác định - - Xác nhận khôi phục module - Hành động này sẽ ghi đè lên tất cả các module hiện có. Tiếp tục? + Sắp xếp (Khởi chạy trước) + Các module sau đây sẽ được cài đặt: %1$s Xác nhận - Thoát - - Sao lưu thành công (tar.gz) - Sao lưu thất bại: %1$s - Sao lưu các module - Khôi phục các module - - Các module đã được khôi phục thành công, cần khởi động lại - Khôi phục thất bại: %1$s - Khởi động lại ngay - Lỗi không xác định - - Thực thi lệnh thất bại: %1$s - - Sao lưu danh sách cho phép thành công - Sao lưu danh sách cho phép thất bại: %1$s - Xác nhận khôi phục danh sách cho phép - Hành động này sẽ ghi đè lên danh sách cho phép hiện tại. Tiếp tục? - Khôi phục danh sách cho phép thành công - Khôi phục danh sách cho phép thất bại: %1$s - Sao lưu danh sách cho phép - Khôi phục danh sách cho phép - Tuỳ chỉnh nền ứng dụng - Chọn một hình ảnh làm hình nền - Độ trong suốt của thanh điều hướng - Phiên bản Android - Model thiết bị + Sắp xếp (Đã bật trước) Không thể cấp quyền Superuser cho %s + Khởi chạy + Đã lưu nhật ký + Vô hiệu hoá khả năng thực thi lệnh SU để lấy quyền root (các app đã cấp trước đó không bị ảnh hưởng). Vô hiệu hoá lệnh SU - Vô hiệu hoá khả năng thực thi lệnh SU để lấy quyền root (Những app đã cấp trước đó không bị ảnh hưởng) - Bạn có chắc muốn cài đặt các module %1$d không? \n\n%2$s - Nhiều cài đặt hơn - SELinux - Đang bật - Đang tắt - Chế độ đơn giản - Ẩn các thẻ không cần thiết ở trang chủ - Ẩn phiên bản Kernel - Ẩn thông tin phiên bản Kernel ở trang chủ - Ẩn thông tin số lượng - Ẩn thông tin về số lượng ở các mục Superuser, Module và KPModule trên thanh điều hướng - Ẩn trạng thái SuSFS - Ẩn thông tin trạng thái SuSFS ở trang chủ - Ẩn trạng thái Zygisk - Ẩn thông tin triển khai Zygisk trên trang chủ - Ẩn trạng thái thẻ liên kết - Ẩn thông tin thẻ liên kết ở trang chủ - Ẩn các nhãn module - Ẩn nhãn tên folder và kích thước trong thẻ module - Chủ đề - Theo hệ thống - Sáng - Tối - Hook thủ công - Màu sắc động - Sử dụng màu sắc động làm chủ đề hệ thống - Chọn màu chủ đề - Xanh dương - Xanh lá - Tím - Cam - Hồng - Xám - Vàng - Cài đặt AnyKernel3 - Flash file AnyKernel3 - Yêu cầu quyền root - Khởi động lại để hoàn tất - Khởi động lại ngay lập tức? - - Không - Khởi động lại thất bại - KPModule - Không có Kernel Module nào được cài đặt tại thời điểm này - Phiên bản - Tác giả - Gỡ cài đặt - Gỡ cài đặt thành công - Gỡ cài đặt thất bại - Load KPModule thành công - Load KPModule thất bại - Thông số - Tuỳ chỉnh - Phiên bản KPM - Đóng - Các chức năng Kernel Module sau đây được KernelPatch phát triển và sửa đổi để tương thích với các chức năng Kernel Module của SukiSU Ultra - Tương lai của SukiSU Ultra - Thành công - Thất bại - SukiSU Ultra sẽ là một nhánh tương đối độc lập của KSU trong tương lai, nhưng chúng tôi xin cảm ơn KernelSU và MKSU,... vì những đóng góp của họ! - Không được hỗ trợ - Được hỗ trợ - Kernel chưa được vá - Kernel chưa được cấu hình - Cài đặt giản lược - Cài đặt KPM - Load - Nhúng - Vui lòng chọn: %1\$s Chế Độ Cài Đặt Module \n\nTải: Tải tạm thời module \nNhúng: Cài đặt vĩnh viễn vào hệ thống - Không thể kiểm tra file module - Màu chủ đề - Loại file không đúng! Vui lòng chọn file .kpm - Gỡ cài đặt - KPM sau đây sẽ được gỡ cài đặt: %s - Sử dụng hai ngón tay để phóng to hình ảnh và một ngón tay kéo thả để điều chỉnh vị trí - Chọn lại - - Flash hoàn tất - - Chuẩn bị… - Đang dọn dẹp các file… - Đang sao chép các file… - Đang giải nén công cụ Flash… - Đang vá tập lệnh Flash… - Flashing Kernel… - Flash hoàn tất - - Chọn Slot Flash - Vui lòng chọn Slot để Flash Boot - Slot A - Slot B - Slot đã chọn: %1$s - Lấy Slot ban đầu - Cài đặt Slot được chỉ định - Khôi phục Slot mặc định - Slot hiện tại: %1$s - - Sao chép thất bại - Lỗi không xác định - Flash thất bại - - LKM cài đặt - Flashing AnyKernel3 - Phiên bản Kernel: %1$s - Sử dụng công cụ vá lỗi: %1$s - Cấu hình - Cài đặt ứng dụng - Công cụ - - Không tìm thấy ứng dụng - SELinux đã bật - SELinux đã tắt - Thay đổi trạng thái SELinux thất bại - Cài đặt nâng cao - Cài đặt giao diện - Trở lại - Đã cài đặt hình nền thành công - Đã xóa hình nền tùy chỉnh - Thay thế icon - Thay đổi icon SukiSU thành icon của KernelSU - Đã thay đổi icon - - Ẩn chức năng KPM - Ẩn thông tin và chức năng của KPM ở trang chủ và thanh điều hướng - - Tuỳ chỉnh WebUI - Tự động chọn - Sử dụng WebUI X - Sử dụng KSU WebUI - Tiêm Eruda vào WebUI X - Chèn bảng điều khiển gỡ lỗi vào WebUI X để gỡ lỗi dễ dàng hơn. Yêu cầu bật gỡ lỗi web - - Tuỳ chỉnh DPI - Điều chỉnh DPI hiển thị - Nhỏ - Vừa - Lớn - Cực lớn - DPI đang hiển thị - Áp dụng - Xác nhận thay đổi DPI - Bạn có chắc chắn muốn thay đổi DPI của ứng dụng từ %1$d thành %2$d? - Ứng dụng cần được khởi động lại để áp dụng cài đặt DPI mới, không ảnh hưởng đến thanh trạng thái hệ thống hoặc các ứng dụng khác - DPI đã được đặt thành %1$d, có hiệu lực sau khi khởi động lại ứng dụng - - Ngôn ngữ ứng dụng - Mặc định theo hệ thống - Độ trong suốt của thẻ - - Error code - Vui lòng kiểm tra nhật ký - Đang cài đặt module %1$d/%2$d - Cài đặt module %d thất bại - Tải xuống module thất bại - Kernel Flashing - - Tất cả - Root - Tuỳ chỉnh - Mặc định - - Tên (Tăng dần) - Tên (Giảm dần) - Thời gian cài đặt (Mới) - Thời gian cài đặt (Cũ) - Kích thước (Giảm dần) - Kích thước (Tăng dần) - Tần suất sử dụng - - Không có ứng dụng nào trong danh mục này - - Huỷ bỏ uỷ quyền - Uỷ quyền - Umount modules - Vô hiệu hoá umount modules - Mở rộng Menu - Thu gọn Menu - Lên trên - Xuống dưới - Đã chọn - Chọn - - Tuỳ chọn Menu - Sắp xếp theo - Lựa chọn loại ứng dụng - - Cấu hình SuSFS - Mô tả cấu hình - Tính năng này cho phép bạn tùy chỉnh giá trị SuSFS Uname và Giả mạo thời gian xây dựng. Nhập các giá trị bạn muốn đặt và nhấp vào \"Áp dụng\" để có hiệu lực - Giá trị Uname - Vui lòng nhập giá trị Uname tùy chỉnh - Giả mạo thời gian xây dựng - Vui lòng nhập giá trị giả mạo thời gian xây dựng - Giá trị hiện tại: %s - Thời gian xây dựng hiện tại: %s - Reset về Default - Áp dụng - - Xác nhận Đặt lại - - Không tìm thấy file ksu_susfs - Thực thi lệnh SuSFS thất bại - Lỗi khi thực hiện lệnh SuSFS: %s - SuSFS Uname và Thời gian xây dựng được thiết lập thành công: %s, %s - - Cấu hình SuSFS - - Tự động khởi động - Tự động áp dụng tất cả các cấu hình không phải mặc định khi khởi động lại - Cấu hình cần được thêm vào để kích hoạt - Kích hoạt tính năng tự động khởi động thất bại! - Vô hiệu hoá tính năng tự động khởi động thất bại! - Lỗi cấu hình tự động khởi động: %s - Không có cấu hình nào có sẵn để tự động khởi động - - Cài đặt cơ bản - Đường dẫn SuS - SuS Mount - Try Umount - Cài đặt Đường dẫn - Trạng thái tính năng - - Thêm Đường dẫn SuS - Thêm SuS Mount - Thêm Try Umount - Đường dẫn SuS đã được thêm thành công - Lỗi không tìm thấy đường dẫn - Đường dẫn - Đường dẫn Mount - Ví dụ: /system/addon.d - Không có Đường dẫn SuS nào được cấu hình - Không có SuS Mount nào được cấu hình - Không có Try Umount nào được cấu hình - - Chế độ Umount - Normal Umount (0) - Detach Umount (1) - Normal - Detach - Chế độ: %1$s (%2$s) - Đường dẫn Try Umount đã thêm thành công: %s - Đường dẫn Try Umount đã lưu thành công: %s - - - Đặt lại Đường dẫn SuS - Thao tác này sẽ xóa tất cả các cấu hình Đường dẫn SuS. Bạn có chắc chắn muốn tiếp tục không? - Đặt lại SuS Mount - Thao tác này sẽ xóa tất cả các cấu hình SuS Mount. Bạn có chắc chắn muốn tiếp tục không? - Đặt lại Try Umount - Thao tác này sẽ xóa tất cả các cấu hình Try Umount. Bạn có chắc chắn muốn tiếp tục không? - Reset Cài đặt Đường dẫn - - Đường dẫn Android Data - Đường dẫn SD Card - Đặt Đường dẫn Android Data - Đặt Đường dẫn SD Card - - Hiển thị trạng thái tính năng hiện tại của SuSFS - Không tìm thấy thông tin trạng thái tính năng - Đã bật - Đã tắt - - Hỗ trợ Đường dẫn SuS - Hỗ trợ SuS Mount - Hỗ trợ Try Umount - Hỗ trợ giả mạo Uname - Giả mạo Cmdline/Bootconfig - Mở hỗ trợ chuyển hướng - Hỗ trợ ghi nhật ký - Tự động Mount mặc định - Tự động Bind Mount - Tự động Try Umount Bind Mount - Ẩn biểu tượng KSU SuSFS - Hỗ trợ SuS Kstat - Chức năng chuyển đổi chế độ SuS SU - - Nhấn để bật/tắt ghi nhật ký - Kích hoạt nhật ký SuSFS - Bật hoặc tắt ghi nhật ký cho SuSFS - Cấu hình ghi nhật ký SuSFS - Bật ghi nhật ký SuSFS - Tắt ghi nhật ký SuSFS - JSON cập nhật - JSON URL cập nhật đã được sao chép vào clipboard - - Hiển thị \"JSON URLs\" - Hiển thị thông tin đường dẫn cập nhật \"JSON URLs\" của module - Vị trí thực thi - Vị trí thực thi hiện tại: %s - Service - Post-FS-Data - Thực thi sau khi dịch vụ hệ thống khởi động - Thực thi sau khi file hệ thống được mount nhưng trước khi hệ thống khởi động hoàn toàn, có thể gây ra boot loop - Thông tin Slot - Xem thông tin Slot khởi động hiện tại và sao chép giá trị - Slot hiện tại: %s - Uname: %s - Thời gian xây dựng: %s - Hiện tại - Sử dụng Uname - Sử dụng Thời gian xây dựng - Không thể lấy thông tin Slot - - Module tự động khởi động SuSFS đã bật, đường dẫn module: %s - Module tự động khởi động SuSFS đã bị vô hiệu hoá - - Cấu hình Kstat - Đã thêm cấu hình Kstat tĩnh: %1$s - Đã xoá cấu hình Kstat: %1$s - Đã thêm Đường dẫn Kstat: %1$s - Đã xoá Đường dẫn Kstat: %1$s - Đã cập nhật Kstat: %1$s - Bản sao Kstat đầy đủ đã cập nhật: %1$s - Thêm cấu hình Kstat tĩnh - Đường dẫn File/Folder - Gợi ý: Bạn có thể sử dụng \"default\" để thiết lập giá trị ban đầu - Thêm Đường dẫn Kstat - Thêm - Đặt lại Cấu hình Kstat - Bạn có chắc chắn muốn xóa tất cả cấu hình Kstat không? Không thể hoàn tác hành động này - Mô tả cấu hình Kstat - • add_sus_kstat_statically: Thông tin thống kê cấu hình tĩnh của các File/Folder - • add_sus_kstat: Thêm đường dẫn trước khi mount để lưu trữ thông tin trạng thái ban đầu - • update_sus_kstat: Cập nhật ino mục tiêu, giữ nguyên kích thước và khối - • update_sus_kstat_full_clone: ​​Chỉ cập nhật ino, giữ nguyên các giá trị gốc khác - Cấu hình Kstat tĩnh - Quản lý Đường dẫn Kstat - Chưa có cấu hình Kstat, hãy nhấp vào nút bên dưới để thêm - - Điều khiển ẩn SuS Mount - Kiểm soát hành vi ẩn của SuS Mount với các tiến trình - Ẩn SuS Mount khỏi tất cả các tiến trình - Khi kích hoạt, SuS Mount sẽ bị ẩn khỏi tất cả các tiến trình, bao gồm cả các tiến trình KSU - Khi bị vô hiệu hoá, SuS Mount sẽ chỉ bị ẩn khỏi các tiến trình không phải KSU, không bị ẩn ở tiến trình KSU - Đã kích hoạt ẩn SuS Mount khỏi tất cả các tiến trình - Đã vô hiệu hoá việc ẩn SuS Mount cho tất cả các tiến trình - Nên vô hiệu hoá sau khi màn hình được mở khoá hoặc trong giai đoạn service.sh hoặc boot-completed.sh, vì điều này sẽ khắc phục sự cố trên một số ứng dụng đã root dựa vào mount bởi tiến trình KSU - Cài đặt hiện tại: %s - Ẩn khỏi tất cả các tiến trình - Chỉ ẩn đối với các tiến trình không phải KSU - Hiển thị tóm tắt \"Phiên bản Kernel\" - Tóm tắt hiển thị phiên bản Kernel cho ngắn gọn - Đường dẫn Android Data đã được đặt thành: %s - Đường dẫn SD Card đã được đặt thành: %s - Thiết lập đường dẫn có thể không thành công hoàn toàn, nhưng đường dẫn SuS sẽ tiếp tục được thêm vào - - Sao lưu - Tạo bản sao lưu cho tất cả các cấu hình SuSFS. File sao lưu sẽ bao gồm tất cả các thiết lập, đường dẫn và thông tin cấu hình - Tạo bản sao lưu - Đã tạo bản sao lưu thành công: %s - Tạo bản sao lưu thất bại: %s - Không tìm thấy file sao lưu - Định dạng file sao lưu không hợp lệ - Phiên bản sao lưu không khớp, nhưng sẽ cố gắng khôi phục - Khôi phục - Khôi phục cấu hình SuSFS từ file sao lưu. Thao tác này sẽ ghi đè lên tất cả các cài đặt hiện tại - Chọn file sao lưu - Cấu hình đã được khôi phục thành công từ bản sao lưu được tạo trên %s, từ thiết bị: %s - Khôi phục thất bại: %s - Xác nhận khôi phục - Thao tác này sẽ ghi đè lên tất cả các cấu hình SuSFS hiện tại. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục - Ngày sao lưu: %s - Thiết bị: %s - Phiên bản: %s - Trạng thái Lock BL - Ghi đè thuộc tính trạng thái lock bootloader ở chế độ dịch vụ late_start - Dọn rác - Dọn dẹp các file và folder còn sót lại của các module và công cụ (Có thể bị xóa nhầm, dẫn đến mất dữ liệu và không khởi động được) - Chỉnh sửa Đường dẫn SuS - Chỉnh sửa SuS Mount - Chỉnh sửa Try Umount - Chỉnh sửa cấu hình Kstat tĩnh - Chỉnh sửa Đường dẫn Kstat - Lưu - Chỉnh sửa - Xoá - Cập nhật - Cập nhật cấu hình Kstat - Cập nhật Đường dẫn Kstat - Cập nhật bản sao SuSFS đầy đủ - Umount dịch vụ cô lập Zygote - Umount các điểm dịch vụ cô lập Zygote khi khởi động hệ thống - Umount dịch vụ cô lập Zygote đã bật - Umount dịch vụ cô lập Zygote đã tắt - Đường dẫn ứng dụng - Đường dẫn khác - Khác - Ứng dụng - Thêm Đường dẫn ứng dụng - Phiên bản thư viện SuSFS không khớp (Kernel: %1$s & Trình quản lý: %2$s). Nên cập nhật Kernel hoặc Trình quản lý - Cảnh báo - Tìm kiếm ứng dụng - %1$d ứng dụng đã chọn - %1$d ứng dụng đã thêm - Tất cả các ứng dụng đã được thêm vào - Cấu hình Trình quản lý động - Đã kích hoạt (Size: %s) - Đã vô hiệu hoá - Kích hoạt Trình quản lý động - Size chữ ký của Trình quản lý động - Hash chữ ký của Trình quản lý động - Hash phải dài 64 ký tự thập lục phân - Cấu hình Trình quản lý động đã được thiết lập thành công - Thiết lập cấu hình Trình quản lý động thất bại - Cấu hình Trình quản lý không hợp lệ - Trình quản lý động đã bị vô hiệu hoá - Xoá Trình quản lý động thất bại - Trình quản lý động - Chữ ký %1$d - Không xác định - Trình quản lý đang hoạt động - Trình quản lý đang không hoạt động - SukiSU - Triển khai Zygisk - - Đường dẫn Vòng lặp SuS - Thêm Đường dẫn Vòng lặp SuS - Chỉnh sửa Đường dẫn Vòng lặp SuS - Đường dẫn Vòng lặp SuS đã thêm thành công: %1$s - Đã xoá Đường dẫn Vòng lặp SuS: %1$s - Đã cập nhật Đường dẫn Vòng lặp SuS: %1$s → %2$s - Không có Đường dẫn Vòng lặp SuS nào được cấu hình - Đặt lại Đường dẫn Vòng lặp SuS - Bạn có chắc chắn muốn xóa tất cả các Đường dẫn Vòng lặp SuS không? Thao tác này không thể hoàn tác - Đường dẫn Vòng lặp - /data/example/path - Lưu ý: Chỉ những đường dẫn KHÔNG nằm trong /storage/ và /sdcard/ mới có thể được thêm vào thông qua Đường dẫn Vòng lặp - Lỗi: Đường dẫn Vòng lặp không thể nằm trong thư mục /storage/ hoặc /sdcard/ - Đường dẫn Vòng lặp - Thêm Đường dẫn Vòng lặp - - Cấu hình Đường dẫn Vòng lặp - Đường dẫn Vòng lặp được đổi tên thành SUS_PATH mỗi khi một ứng dụng không phải root hoặc dịch vụ cô lập được khởi động. Điều này giúp giải quyết vấn đề đường dẫn đã thêm có thể trở nên không hợp lệ do trạng thái inode được đặt lại hoặc inode được tạo lại trong Kernel - Giả mạo nhật ký AVC - Giả mạo nhật ký AVC đã kích hoạt - Giả mạo nhật ký AVC đã bị vô hiệu hoá - Tắt: Vô hiệu hoá tính năng giả mạo sus tcontext của \'su\' trong nhật ký AVC của kernel\n -Bật: Kích hoạt tính năng giả mạo sus tcontext của \'su\' thành \'kernel\' trong nhật ký AVC của kernel - Lưu ý quan trọng:\n -- Giá trị này được đặt thành \'0\' theo mặc định trong kernel\n -- Việc bật tính năng này đôi khi có thể khiến các nhà phát triển khó xác định nguyên nhân của các vấn đề về quyền hoặc SELinux khi gỡ lỗi, vì vậy người dùng nên tắt tính năng này khi gỡ lỗi - - Đã xác minh - Chữ ký module đã được xác minh - Xác minh chữ ký - Buộc xác minh chữ ký khi cài đặt module (Chỉ khả dụng cho kiến trúc ARM) - Tác giả không xác định - Các module chưa được ký có thể chưa hoàn chỉnh. Để bảo vệ thiết bị của bạn, module này đã bị chặn cài đặt - Các module chưa được ký có thể chưa hoàn chỉnh. Bạn có muốn cài đặt module này từ một tác giả chưa xác định không? - Chế độ Hook - - Vá KPM - Thêm tính năng KPM - Vá KPM - Thực hiện Vá KPM vào kernel trước khi flash - Hoàn tác Vá KPM - Hoàn tác Vá KPM đã áp dụng trước đó - Vá KPM đã bật - Hoàn tác Vá KPM đã bật - Chế độ Vá KPM - Chế độ hoàn tác Vá KPM - - Chuẩn bị công cụ Vá KPM - Áp dụng Vá KPM - Hoàn tác Vá KPM - Đã tìm thấy file image: %s - Đã vá KPM thành công - Hoàn tác vá KPM thành công - Nén lại file thành công - - Giải nén file thất bại - Không tìm thấy file image - Vá KPM thất bại - Hoàn tác Vá KPM thất bại - Quá trình Vá KPM thất bại: %s - - Mặc định theo file Kernel - Sử dụng file kernel mặc định mà không có bất kỳ sửa đổi nào về KPM - - Chế độ quét danh sách ứng dụng người dùng - Bật tuỳ chọn này thì chế độ người dùng sẽ được sử dụng để quét danh sách ứng dụng nhằm cải thiện tính ổn định (Nếu danh sách ứng dụng quét kernel bị kẹt và xảy ra các sự cố khác, bạn có thể thử bật tùy chọn này) - Quét ứng dụng nhiều người dùng - Khi được bật, tất cả ứng dụng của người dùng sẽ được quét, bao gồm cả dữ liệu công việc, v.v - Thiết lập thất bại, vui lòng kiểm tra quyền - Dọn dẹp môi trường hoạt động - Dọn dẹp các file hoạt động và dừng quét các dịch vụ - Bạn có chắc chắn muốn dọn dẹp môi trường hoạt động không? Thao tác này sẽ dừng dịch vụ quét và xóa các file liên quan - Dọn dẹp môi trường hoạt động thành công - Dọn dẹp môi trường hoạt động thất bại - - Xác nhận cài đặt - Xác nhận cài đặt (%d files) - Cài đặt - Module - Kernel - Không xác định - Kernel không xác định - Tệp không xác định - Phiên bản - Tác giả - Mô tả - Thiết bị được hỗ trợ - - SuS Maps - Đường dẫn thư viện - /data/adb/modules/my_module/zygisk/arm64-v8a.so - Thêm SuS Map - Chỉnh sửa SuS Map - Đã thêm SuS Map thành công: %1$s - Đã xoá SuS Map: %1$s - Đã cập nhật SuS Map: %1$s → %2$s - Không có SuS Map nào được cấu hình - Đặt lại SuS Map - Thao tác này sẽ xóa tất cả các SuS Map đã cấu hình. Không thể hoàn tác thao tác này - Ẩn Bộ nhớ Map - Ẩn tệp mmapp khỏi các map khác nhau trong /proc/self/ - Ẩn đường dẫn tệp của bộ nhớ map khỏi /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Lưu ý: Tính năng này không hỗ trợ ẩn bộ nhớ map ẩn danh, cũng như không thể ẩn các hook nội tuyến hoặc hook PLT do chính thư viện được đưa vào gây ra - Quan trọng: Đối với các ứng dụng có cơ chế phát hiện tấn công mạnh mẽ, tính năng này có thể không hiệu quả trong việc bỏ qua khả năng phát hiện - Trước tiên, hãy tìm PID và UID của ứng dụng đích bằng lệnh ps -enf. Sau đó kiểm tra các đường dẫn liên quan trong /proc/<pid>/maps và so sánh chúng với số thiết bị trong /proc/1/mountinfo để đảm bảo tính nhất quán. Chỉ khi số thiết bị khớp nhau thì chức năng ẩn map mới hoạt động bình thường - - Trình xem nhật ký - Trở lại - Tìm kiếm - Xoá nhật ký - Bạn có chắc chắn muốn xóa tệp nhật ký đã chọn không? Thao tác này không thể hoàn tác - Đã xoá nhật ký thành công - Lọc theo loại - Tất cả các loại - Hiển thị %1$d trong tổng %2$d nhật ký - Không tìm thấy nhật ký nào - Không tìm thấy nhật ký nào phù hợp - Làm mới - Raw Log - Tìm kiếm theo UID, lệnh hoặc chi tiết… - Xoá tìm kiếm - Xem nhật ký sử dụng - Xem nhật ký truy cập Superuser - Loại trừ các phân loại - Ứng dụng hiện tại - Trang: %1$d/%2$d | Tổng nhật ký: %3$d - Quá nhiều nhật ký, chỉ hiển thị %1$d nhật ký mới nhất - Đang load thêm nhật ký - Tất cả nhật ký đã được hiển thị - - Bạn muốn xoá tôi đi sao 😭 - Hừm, được rồi, tôi sẽ bị bạn gỡ cài đặt. Chức năng root sẽ không ngừng hoạt động chỉ vì bạn mất một Trình quản lý. Đừng lo chỉ Gỡ cài đặt Trình quản lý thôi thì không thể mất quyền truy cập root được đâu, zako~❤️ + Kiểm tra cập nhật module + Sử dụng file LKM cục bộ + Chỉ hỗ trợ các file .ko + Vô hiệu hoá umount kernel + Vô hiệu hoá umount kernel-level được kiểm soát bởi KernelSU. + Kích hoạt bảo mật nâng cao + Cho phép chính sách bảo mật chặt chẽ hơn. + Mặc định + Tạm thời kích hoạt + Luôn luôn kích hoạt + Đang xử lý… + Kéo xuống để làm mới + Thả để làm mới + Đang làm mới… + Đã làm mới thành công
diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index ea774599..36d2398a 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -4,12 +4,13 @@ 未安装 点击安装 工作中 - 版本:%s + 版本:%d + 超级用户数:%d 不支持 内核上未检测到 KernelSU 驱动程序,内核错误? 内核版本 - SuSFS 版本 管理器版本 + 系统指纹 SELinux 状态 被禁用 强制执行 @@ -20,8 +21,8 @@ 无法禁用模块:%s 没有安装模块 模块 - 排序(可执行优先) - 排序(已启用优先) + 可执行优先 + 已启用优先 卸载 安装 安装 @@ -29,15 +30,17 @@ 设置 软重启 重启到 Recovery - 重启到 Bootloader + 重启到 BootLoader 重启到 Download 重启到 EDL 关于 确定要卸载模块 %s 吗? + "您确定要卸载模块 %s 吗?此操作将影响所有模块,并且元模块提供的某些功能(如挂载)将不再工作。 " %s 已卸载 卸载失败:%s 版本 作者 + OverlayFS 被内核禁用,模块不可用! 刷新 显示系统应用 隐藏系统应用 @@ -45,6 +48,7 @@ 安全模式 重启生效 因与 Magisk 有冲突,所有模块不可用! + 模块数:%d 了解 KernelSU https://kernelsu.org/zh_CN/guide/what-is-kernelsu.html 了解如何安装 KernelSU 以及如何开发模块 @@ -55,28 +59,33 @@ 模版 自定义 名称 + 命名空间 + 继承 + 全局 + 私有 权能 SELinux 卸载模块 为 %s 更新 App Profile 失败 - 当前 KernelSU 版本 %s 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %s 或以上! + 当前 KernelSU 版本 %d 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %d 或以上! 默认卸载模块 - App Profile 中\"卸载模块\"的全局默认值,如果启用,将会为没有设置 Profile 的应用移除所有模块针对系统的修改。 + App Profile 中“卸载模块”的全局默认值,如果启用,将会为没有设置 Profile 的应用移除所有模块针对系统的修改。 启用该选项后将允许 KernelSU 为本应用还原被模块修改过的文件。 规则 更新 正在下载模块:%s 开始下载:%s - 发现新版本:%s,点击升级。 + 发现新版本:%s,点击升级! 启动 - 强制停止 + 强制停止 重新启动 - 为:%s 更新 SELinux 规则失败 + 为 %s 更新 SELinux 策略失败 + 无法授予 %s 超级用户权限 更新日志 App Profile 模版 - 管理本地和在线的 App Profile 模版 + 管理本地和在线的 App Profile 模版。 创建模版 编辑模版 模版 ID @@ -96,16 +105,18 @@ 同步在线规则 模版保存失败 剪切板为空! + 影响以下应用 获取更新日志失败:%s 检查更新 - 在应用启动后自动检查是否有最新版 + 在应用启动后自动检查是否有最新版。 + 检查模块更新 获取 root 失败! 执行 - 关闭 - 启用 WebView 调试 + 打开 + WebView 调试 可用于调试 WebUI 。请仅在需要时启用。 直接安装(推荐) - 选择一个需要修补的镜像 + 选择一个文件 安装到未使用的槽位(OTA 后) 将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。 下一步 @@ -120,49 +131,14 @@ 恢复原厂镜像 临时卸载 KernelSU,下次重启后恢复至原始状态。 完全并永久卸载 KernelSU(Root 权限和所有模块)。 - 恢复原厂镜像(若存在备份),一般在 OTA 前使用;如果你需要卸载 KernelSU,请使用\"永久卸载\"。 + 恢复原厂镜像(若存在备份),一般在 OTA 前使用;如果你需要卸载 KernelSU,请使用“永久卸载”。 刷写中 刷写完成 刷写失败 已选择的 LKM:%s 保存日志 日志已保存 - - 确认安装模块 %1$s? - 未知模块 - - 确认还原模块 - 此操作将覆盖所有现有模块,是否继续? - 确定 - 取消 - - 备份成功 (tar.gz) - 备份失败:%1$s - 备份模块 - 恢复模块 - - 模块已成功还原,需重启生效 - 还原失败:%1$s - 立即重启 - 未知错误 - - 命令执行失败:%1$s - - 应用列表备份成功 - 应用列表备份失败:%1$s - 确认还原应用列表 - 此操作将覆盖当前的应用列表,是否继续? - 应用列表还原成功 - 应用列表还原失败:%1$s - 备份应用列表 - 还原应用列表 - 自定义背景 - 选择一张图片作为应用背景 - 卡片不透明度 - Android 版本 - 设备 - 不允许授予 %s 超级用户权限 - 禁用 su 兼容性 + 关闭 su 兼容 禁止任何应用通过 su 命令获取 root 权限(已运行的 root 进程不受影响)。 关闭内核 umount 关闭 KernelSU 控制的内核级 umount 行为。 @@ -171,575 +147,15 @@ 默认 临时启用 始终启用 - 确定要安装以下 %1$d 个模块吗?\n\n%2$s - 更多设置 - SELinux - 强制执行 - 宽容模式 - 简洁模式 - 开启后将隐藏不必要的卡片 - 隐藏内核版本号 - 隐藏内核部分的 KernelSU 版本号 - 强迫症开关 - 隐藏导航栏上的超级用户数、模块数和 KPM 模块数红点 - 隐藏 SuSFS 状态信息 - 隐藏主页上的 SuSFS 状态信息 - 隐藏 Zygisk 状态信息 - 隐藏主页上的 Zygisk 实现状态信息 - 隐藏链接卡片 - 隐藏主页上的链接卡片信息 - 隐藏模块标签行 - 隐藏模块卡片中的文件夹名称和大小标签 - 主题模式 - 跟随系统 - 浅色 - 深色 - Manual Hook - 动态颜色 - 使用系统主题的动态颜色 - 选择主题色 - 蓝色 - 绿色 - 紫色 - 橙色 - 粉色 - 高级灰 - 黄色 - 刷写 AnyKernel3 压缩包 - 刷入 Anykernel3 内核 - 需要 root 权限 - 刷写完成 - 是否立即重启? - - - 重启失败 - 内核模块 - 暂无已安装的内核模块 - 版本 - 作者 - 卸载 - 卸载成功 - 卸载失败 - 加载 KPM 模块成功 - 加载 KPM 模块失败 - 参数 - 调参 - KPM 版本 - 关闭 - 以下内核模块功能由 KernelPatch 开发,经过修改后加入 SukiSU Ultra 的内核模块功能 - SukiSU Ultra 展望 - 成功 - 错误 - SukiSU Ultra 未来将会成为一个相对独立的 KSU 分支,但是依然感谢官方 KernelSU 和 MKSU 等做出的贡献 - 不支持 - 支持 - 内核未进行补丁 - 内核未配置 - 个性化设置 - 安装 - 加载 - 嵌入 - 请选择: %1\$s 模块的安装模式 \n\n加载:临时加载模块\n嵌入:永久安装到系统 - 无法检查模块文件是否存在 - 主题颜色 - 文件类型不正确,请选择 .kpm 文件 - 卸载 - 将卸载以下 KPM 模块:\n%s - 使用双指缩放图片,单指拖动调整位置 - 重置 - - 刷写完成 - - 准备中… - 清理文件… - 复制文件… - 提取刷写工具… - 修补刷写脚本… - 刷写内核中… - 刷写完成 - - 选择刷写槽位 - 请选择要刷写 boot 的目标槽位 - A 槽位 - B 槽位 - 已选择槽位: %1$s - 获取原有槽位 - 设置指定槽位 - 恢复默认槽位 - 当前系统默认槽位:%1$s - - 复制失败 - 未知错误 - 刷写失败 - - LKM 修补/安装 - 刷写 AnyKernel3 - 内核版本:%1$s - 使用修补工具:%1$s - 配置 - 应用设置 - 工具 - - 未找到应用 - SELinux 已设置为启用状态 - SELinux 已设置为禁用状态 - SELinux 状态更改失败 - 高级设置 - 外观设置 - 返回 - 背景设置成功 - 已移除自定义背景 - 备选图标 - 更换为 KernelSU 图标 - 已切换图标 - - 隐藏 KPM 功能 - 在主页和底栏隐藏 KPM 相关功能和信息 - - 选择使用的 WebUI 引擎 - 自动选择 - 强制使用 WebUI X - 强制使用 KSU 的 WebUI - 将 Eruda 注入 WebUI X - 在 WebUI X 中注入调试控制台,使调试更容易,需要启用 WebView 调试 - - 应用 DPI - 仅调整当前应用的屏幕显示密度 - - - - 超大 - 自定义 - 应用 DPI 设置 - 确认更改 DPI - 你确定要将应用 DPI 从 %1$d 更改为 %2$d 吗? - 应用需要重启以应用新的 DPI 设置,不会影响系统状态栏或其他应用 - DPI 已设置为 %1$d,重启应用后生效 - - 应用语言 - 跟随系统 - 卡片暗度调节 - - 错误代码 - 请查看日志 - 正在安装模块 %1$d/%2$d - %d 个模块安装失败 - 模块下载失败 - 内核刷写 - - 全部 - Root - 自定义 - 默认 - - 名称升序 - 名称降序 - 安装时间(新) - 安装时间(旧) - 大小降序 - 大小升序 - 使用频率 - - 此分类中没有应用 - - 取消授权 - 授权 - 卸载模块挂载 - 禁用卸载模块挂载 - 展开菜单 - 收起菜单 - 顶部 - 底部 - 已选择 - 选择 - - 菜单选项 - 排序方式 - 应用类型选择 - - SuSFS 配置 - 配置说明 - 此功能允许您自定义 SuSFS 的 uname 值和构建时间伪装。输入您想要设置的值,点击应用即可生效 - Uname 值 - 请输入自定义 uname 值 - 构建时间伪装 - 请输入构建时间伪装值 - 当前值: %s - 当前构建时间: %s - 重置为默认值 - 应用 - - 确认重置 - - 无法找到 ksu_susfs 文件 - SuSFS 命令执行失败 - 执行 SuSFS 命令时出错: %s - SuSFS 内核名称和构建时间设置成功: %s, %s - - SuSFS 配置 - - 开机自启动 - 重启时自动应用所有非默认配置 - 需要添加配置后才能启用 - 启用开机自启动失败 - 禁用开机自启动失败 - 开机自启动配置错误: %s - 没有可用的配置进行开机自启动 - - 基本设置 - SuS 路径 - SuS 挂载 - 尝试卸载 - 路径设置 - 启用功能状态 - - 添加 SuS 路径 - 添加 SuS 挂载 - 添加尝试卸载 - SuS 路径添加成功 - 路径未找到错误 - 路径 - 挂载路径 - 例如: /system/addon.d - 暂无 SuS 路径配置 - 暂无 SuS 挂载配置 - 暂无尝试卸载配置 - - 卸载模式 - 普通卸载(0) - 分离卸载(1) - 普通 - 分离 - 模式: %1$s (%2$s) - 尝试 umount 路径添加成功: %s - 尝试 umount 路径保存成功: %s - - - 重置 SuS 路径 - 这将清除所有 SuS 路径配置,确定要继续吗? - 重置 SuS 挂载 - 这将清除所有 SuS 挂载配置,确定要继续吗? - 重置尝试卸载 - 这将清除所有尝试卸载配置,确定要继续吗? - 重置路径设置 - - Android Data 路径 - SDCard 路径 - 设置 Android Data 路径 - 设置 SDCard 路径 - - 显示当前 SuSFS 启用的功能状态 - 未找到功能状态信息 - 已启用 - 已禁用 - - SuS 路径支持 - SuS 挂载支持 - 尝试卸载支持 - 欺骗 uname 支持 - 欺骗 Cmdline/Bootconfig - 开放重定向支持 - 日志记录支持 - 自动默认挂载 - 自动绑定挂载 - 自动尝试卸载绑定挂载 - 隐藏 KSU SuSFS 符号 - SuS Kstat 支持 - SuS SU 模式切换功能 - - 可配置的 SuSFS 功能 - SuSFS 启用日志 - 启用或者关闭 SuSFS 的日志 - SuSFS 日志配置 - 启用 SuSFS 日志 - 关闭 SuSFS 日志 - 更新配置 - 更新配置地址已复制到剪贴板 - - 显示更多模块信息 - 显示额外的模块信息,如更新配置 URL 等 - 执行位置 - 当前执行位置:%s - Service - Post-FS-Data - 在系统服务启动后执行 - 在文件系统挂载后但系统完全启动前执行,可能会导致循环重启 - 槽位信息 - 查看当前启动槽位信息并复制数值 - 当前活动槽位:%s - Uname:%s - 构建时间:%s - 当前 - 使用 Uname - 使用构建时间 - 无法获取槽位信息 - - SuSFS 自启动模块已启用,模块路径:%s - SuSFS 自启动模块已禁用 - - Kstat 配置 - Kstat 静态配置已添加:%1$s - 已移除 Kstat 配置:%1$s - Kstat 路径已添加:%1$s - 已移除 Kstat 路径:%1$s - Kstat 已更新:%1$s - Kstat 完整克隆已更新:%1$s - 添加 Kstat 静态配置 - 文件/目录路径 - 提示:可以使用 “default” 来使用原始值 - 添加 Kstat 路径 - 添加 - 重置 Kstat 配置 - 确定要清除所有 Kstat 配置吗?此操作不可撤销 - Kstat 配置说明 - • add_sus_kstat_statically: 静态配置文件/目录的 stat 信息 - • add_sus_kstat: 在绑定挂载前添加路径,存储原始 stat 信息 - • update_sus_kstat: 更新目标 ino,保持 size 和 blocks 不变 - • update_sus_kstat_full_clone: 仅更新 ino,其他保持原始值 - 静态 Kstat 配置 - Kstat 路径管理 - 暂无 Kstat 配置,点击下方按钮添加配置 - - SuS 挂载隐藏控制 - 控制 SuS 挂载对进程的隐藏行为 - 对所有进程隐藏 SuS 挂载 - 启用后,SuS 挂载将对所有进程隐藏,包括 KSU 进程 - 禁用后,SuS 挂载仅对非 KSU 进程隐藏,KSU 进程可以看到挂载 - 已启用对所有进程隐藏 SuS 挂载 - 已禁用对所有进程隐藏 SuS 挂载 - 建议在屏幕解锁后或在 service.sh 或 boot-completed.sh 阶段设置为禁用,这可以修复一些依赖 KSU 进程挂载的 root 应用的问题 - 当前设置: %s - 对所有进程隐藏 - 仅对非 KSU 进程隐藏 - 内核版本简洁模式 - 启用或禁用 SukiSU 内核版本显示的简洁模式 - Android Data 路径已设置为: %s - SDCard 路径已设置为: %s - 路径设置可能未完全成功,但将继续添加 SuS 路径 - - 备份 - 创建所有 SuSFS 配置的备份。备份文件将包含所有设置、路径和配置信息。 - 创建备份 - 备份创建成功:%s - 备份创建失败:%s - 备份文件未找到 - 无效的备份文件格式 - 备份版本不匹配,但将尝试还原 - 还原 - 从备份文件还原 SuSFS 配置。这将覆盖所有当前设置。 - 选择备份文件 - 配置还原成功,备份创建于 %s,来自设备:%s - 还原失败:%s - 确认还原 - 这将覆盖所有当前的 SuSFS 配置。您确定要继续吗? - 还原 - 备份日期:%s - 设备:%s - 版本:%s - 上锁状态 - 覆盖引导锁状态属性于 late_start 服务模式 - 清理工具残留 - 清理各种模块以及工具的残留文件和目录(可能会误删导致丢失以及无法启动,谨慎使用) - 编辑 SuS 路径 - 编辑 SuS 挂载 - 编辑尝试卸载 - 编辑 Kstat 静态配置 - 编辑 Kstat 路径 - 保存 - 编辑 - 删除 - 更新 - Kstat 配置更新 - Kstat 路径更新 - Susfs 完整克隆更新 - 卸载 Zygote 隔离服务 - 启用此选项将在系统启动时卸载 Zygote 隔离服务挂载点 - Zygote 隔离服务卸载已启用 - Zygote 隔离服务卸载已禁用 - 应用路径 - 其他路径 - 其他 - 应用 - 添加应用路径 - SuSFS 库版本不匹配,内核:%1$s vs 管理器:%2$s,建议更新内核或管理器 - 警告 - 搜索应用 - %1$d 个已选应用 - %1$d 个已添加应用 - 所有应用均已添加 - 动态管理器配置 - 已启用(大小: %s) - 未启用 - 启用动态管理器 - 动态管理器签名大小 - 动态管理器签名哈希值 - 哈希值必须是 64 位十六进制字符 - 动态管理器配置设置成功 - 动态管理器配置设置失败 - 无效的签名配置 - 动态管理器已禁用 - 清除动态管理器错误 - 动态 - 签名 %1$d - 未知 - 活跃管理器 - 无活跃管理器 - SukiSU - Zygisk 实现 - - SuS 循环路径 - 添加 SuS 循环路径 - 编辑 SuS 循环路径 - SuS 循环路径添加成功: %1$s - SuS 循环路径已移除: %1$s - SuS 循环路径已更新: %1$s -> %2$s - 未配置 SuS 循环路径 - 重置循环路径 - 确定要清空所有 SuS 循环路径吗?此操作无法撤销。 - 循环路径 - /data/example/path - 注意:只有不在 /storage/ 和 /sdcard/ 内的路径才能通过循环路径添加。 - 错误:循环路径不能位于 /storage/ 或 /sdcard/ 目录内 - 循环路径 - 添加循环路径 - - 循环路径配置 - 循环路径会在每次非 root 用户应用或隔离服务启动时重新标记为 SUS_PATH。这有助于解决添加的路径可能因 inode 状态重置或内核中 inode 重新创建而失效的问题 - AVC 日志欺骗 - AVC 日志欺骗已启用 - AVC 日志欺骗已禁用 - 禁用: 禁用在内核 AVC 日志中欺骗 \'su\' 的 sus tcontext。\n -启用: 启用在内核 AVC 日志中将 \'su\' 的 sus tcontext 欺骗为 \'kernel\' - 重要提示:\n -- 内核中默认设置为 \'0\'\n -- 启用此功能有时会使开发人员在调试权限或 SELinux 问题时难以识别原因,因此建议用户在调试时禁用此功能。 - - 已验证 - 模块签名已验证 - 验证签名 - 模块安装时,强制验证签名。(仅 ARM架构 可用) - 未知发布者 - 未经签名的模块可能不完整。为了对设备进行保护,已阻止安装此模块。 - 未经签名的模块可能不完整。你想安装来自未知发布者的模块吗? - 钩子类型 - - KPM 修补 - 用于添加附加的 KPM 功能 - KPM 修补 - 在刷写前对内核镜像进行KPM修补 - KPM 撤销修补 - 撤销之前应用的KPM修补 - KPM 修补已启用 - KPM 撤销修补已启用 - KPM 修补模式 - KPM 撤销修补模式 - - 准备 KPM 修补工具 - 正在应用 KPM 修补 - 正在撤销 KPM 修补 - 找到 Image 文件: %s - KPM 修补成功 - KPM 撤销修补成功 - 文件重新打包完成 - - 解压压缩包失败 - 未找到 Image 文件 - KPM 修补失败 - KPM 撤销修补失败 - KPM 修补操作失败: %s - - 跟随内核 - 原样使用内核,不进行任何 KPM 修改 - - 用户态扫描应用列表 - 开启后将使用用户态扫描应用列表,提高稳定性 (因内核扫描应用列表出现卡死等问题可以尝试打开此选项) - 多用户应用扫描 - 开启后将扫描所有用户的应用,包括工作资料等 - 设置失败,请检查权限 - 清理运行环境 - 清理运行时文件并停止扫描服务 - 您确定要清理运行环境吗?这将停止扫描服务并删除相关文件 - 运行环境清理成功 - 运行环境清理失败 - - 确认安装 - 确认安装(%d 个文件) - 确认安装 - 模块 - 内核 - 未知类型 - 未知内核 - 未知文件 - 版本 - 作者 - 描述 - 支持设备 - - SUS映射 - 库文件路径 - /data/adb/modules/my_module/zygisk/arm64-v8a.so - 添加SUS映射 - 编辑SUS映射 - SUS映射添加成功: %1$s - SUS映射已移除: %1$s - SUS映射已更新: %1$s -> %2$s - 未配置SUS映射 - 重置SUS映射 - 这将移除所有已配置的SUS映射。此操作无法撤销。 - 内存映射隐藏 - 隐藏/proc/self/中各种映射中的mmap真实文件 - 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 - 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 - 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 - - 日志查看器 - 返回 - 搜索 - 清空日志 - 确定要清空选中的日志文件吗?此操作无法撤销。 - 日志清空成功 - 按类型筛选 - 所有类型 - 显示 %1$d / %2$d 条记录 - 未找到日志 - 未找到匹配的日志 - 刷新 - 原始日志 - 按 UID、命令或详情搜索… - 清除搜索 - 查看使用日志 - 查看 KernelSU 超级用户访问日志 - 排除子类型 - 当前应用 - 页面: %1$d/%2$d | 总日志: %3$d - 日志过多,仅显示最新 %1$d 条 - 加载更多日志 - 已显示所有日志 - - 真要走? - 哼,卸就卸。Root 功能可不会因为失去区区一个管理器就停止运作。别担心,zakozako 只卸载管理器可干不掉 Root 呢,zako~❤️ - 当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d) - Umount 路径管理 - 管理内核卸载路径 - 添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。 - 添加 Umount 路径 - 挂载路径 - 卸载标志 - 0=正常卸载, 8=MNT_DETACH, -1=自动 - 标志 - 默认条目 - 确认删除 - 确定要删除路径 %s 吗? - 路径已添加,重启后生效 - 路径已删除,重启后生效 - 操作失败 - 确认操作 - 确定要清除所有自定义路径吗?(默认路径将保留) - 自定义路径已清除 - 清除自定义 - 应用配置 - 配置已应用到内核 + 将安装以下模块:%1$s + 确认 + 处理中… + 下拉刷新 + 松开刷新 + 正在刷新… + 刷新成功 + 撤销 + 成功撤销卸载 %s + 撤销卸载 %s 失败 包含 %1$d 个应用 - 禁用超级用户日志 - 禁用 KernelSU 超级用户访问记录 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index e78faec6..6ac997ff 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -1,603 +1,127 @@ - 主頁 + 首頁 未安裝 - 點擊安裝 - 工作中 - 版本:%s - 唔支援 - 核心上未檢測到 KernelSU 驅動程式,核心錯誤? - 核心版本 - SuSFS 版本 + 按一下以安裝 + 運作中 + KernelSU 版本:%d + 超級使用者:%d 個 + 已安裝模組:%d 個 + 不支援 + KernelSU 現在僅支援 GKI 核心 + 核心 管理器版本 + 指紋 SELinux 狀態 - 被禁用 - 強制執行 - 寬容模式 + 已停用 + 強制 + 寬鬆 未知 - 超級用戶 + 超級使用者 無法啟用模組:%s - 無法禁用模組:%s - 冇安裝模組 + 無法停用模組:%s + 尚未安裝模組 模組 - 排序(可執行優先) - 排序(已啟用優先) - 卸載 + 解除安裝 安裝 安裝 重新啟動 - 配置 - 軟重啟 - 重新啟動到 Recovery - 重新啟動到 Bootloader - 重新啟動到 Download - 重新啟動到 EDL + 設定 + 軟啟動 + 重新啟動至 Recovery + 重新啟動至 Bootloader + 重新啟動至 Download + 重新啟動至 EDL 關於 - 確定要卸載模組 %s 嗎? - %s 已卸載 - 卸載失敗:%s + 您確定要解除安裝模組「%s」嗎? + 「%s」已解除安裝 + 無法解除安裝:%s 版本 作者 - 刷新 - 顯示系統應用 - 隱藏系統應用 - 發送日誌 + OverlayFS 無法使用,模組無法正常運作! + 重新整理 + 顯示系統應用程式 + 隱藏系統應用程式 + 傳送記錄 安全模式 - 重新啟動後生效 - 因同 Magisk 有衝突,所有模組唔可用! - 了解 KernelSU - https://kernelsu.org/zh_CN/guide/what-is-kernelsu.html - 了解如何安裝 KernelSU 以及如何開發模組 + 重新啟動以生效 + 模組已停用,因其與 Magisk 的模組存在衝突! + 深入瞭解 KernelSU + https://kernelsu.org/zh_TW/guide/what-is-kernelsu.html + 瞭解如何安裝 KernelSU 以及如何開發模組 支援開發 - KernelSU 將保持免費開源,向開發者捐贈以表示支援。 - 加入我哋嘅 %2$s 頻道]]

有動漫人物圖片表情包嘅圖像版權為%3$s所有,圖像中嘅知識產權由%4$s 所有。喺使用這些文件之前,除了必須遵守 %5$s 以外,還需要遵守向前兩者索要使用這些藝術內容嘅授權。]]>
- 默認 - 模版 - 自定義 - 名稱 - - 權限 - SELinux + KernelSU 將保持免費和開源,您可以考慮向開發人員贊助以表示支持。 + 加入我們的 %2$s 頻道]]> + 預設 + 設定檔名稱 + 範本 + 繼承 + 全域 + 功能 卸載模組 - 為 %s 更新 App Profile 失敗 - 當前 KernelSU 版本 %s 過低,管理器無法正常工作,請將核心 KernelSU 版本升級至 %s 或以上! - 默認卸載模組 - App Profile 中\"卸載模組\"嘅全局默認值,如果啟用,將會為冇設定 Profile 嘅應用移除所有模組針對系統嘅修改。 - 啟用該選項後將允許 KernelSU 為本應用還原被模組修改過嘅文件。 - + 無法更新 %s 應用程式設定檔 規則 + 目前 KernelSU 版本 %d 過低,管理器無法正常運作。請升級至 %d 或更高版本! + 應用程式設定檔中「解除安裝模組」的全域預設值,如果啟用,將會為沒有設定檔的應用程式移除所有模組針對系統的修改。 + 啟用此選項將允許 KernelSU 為這個應用程式還原任何被模組修改過的檔案。 + 網域 更新 + 自訂 + 掛載命名空間 + 個人 + 群組 + SELinux 環境 + 預設解除安裝模組 正在下載模組:%s 開始下載:%s - 發現新版本:%s,點擊升級。 + 新版本:%s 已可供使用,按一下以升級 啟動 - 強制停止 + 強制停止 重新啟動 - 為:%s 更新 SELinux 規則失敗 - 更新日誌 - App Profile 模版 - 管理本地同在線嘅 App Profile 模版 - 創建模版 - 編輯模版 - 模版 ID - 模版 ID 唔合法 - 名字 - 描述 - 存儲 - 刪除 - 查看模版 - 只讀 - 模版 ID 已存在! - 導入/導出 - 從剪貼板導入 + 無法為 %s 更新 SELinux 規則 + 變更記錄 + 成功匯出 導出到剪貼板 - 冇可以導出嘅本地模板! - 導入成功 - 同步在線規則 - 模版存儲失敗 - 剪貼板為空! + 本地沒有模板可匯出! + 模板 ID 已存在! + 從剪貼簿匯入 獲取更新日誌失敗:%s + 名字 + 模板 ID 無效 + 同步在線規則 + 創建模板 + 只讀 + 匯出 / 匯入 + 模板儲存失敗 + 編輯模板 + 模板 ID + App Profile 模板 + 描述 + 儲存 + 管理本地和線上的 App Profile 模板 + 刪除 + 剪貼簿沒有內容! + 查看模板 + 啟用 WebView 偵錯 + 可用於偵錯WebUI,請僅在需要時啟用。 + 直接安裝(建議) + 選擇一個文件 + 安裝到非活動插槽(OTA 後) + 重新啟動後,您的裝置將強制啟動到目前非活動插槽! +\n僅在 OTA 完成後使用此選項。 +\n繼續? + 下一個 + 選擇KMI + 建議使用 %1$s 分割區映像 + 授予root權限失敗! + 打開 檢查更新 - 喺應用啟動後自動檢查是否有最新版 - 獲取 root 失敗! - 執行 - 關閉 - 啟用 WebView 調試 - 可用於調試 WebUI 。請僅喺需要時啟用。 - 直接安裝(推薦) - 選擇一個需要修補嘅鏡像 - 安裝到未使用嘅槽位(OTA 後) - 將喺重新啟動後強制切換到另一個槽位!\n注意只能喺 OTA 更新完成後嘅重新啟動之前使用。\n確認? - 下一步 - 建議選擇 %1$s 分區鏡像 - 選擇 KMI - 卸載 - 臨時卸載 - 永久卸載 - 恢復原廠鏡像 - 臨時卸載 KernelSU,下次重新啟動後恢復至原始狀態。 - 完全並永久卸載 KernelSU(Root 權限同所有模組)。 - 恢復原廠鏡像(若存在備份),一般喺 OTA 前使用;如果您需要卸載 KernelSU,請使用\"永久卸載\"。 - 刷寫中 - 刷寫完成 - 刷寫失敗 - 選擇嘅 LKM:%s - 存儲日誌 - 日誌已存儲 - - 確認安裝模組 %1$s? - 未知模組 - - 確認還原模組 - 此操作將覆蓋所有現有模組,是否繼續? - 確定 - 取消 - - 備份成功 (tar.gz) - 備份失敗:%1$s - 備份模組 - 恢復模組 - - 模組已成功還原,需重新啟動生效 - 還原失敗:%1$s - 立即重新啟動 - 未知錯誤 - - 命令執行失敗:%1$s - - 應用列表備份成功 - 應用列表備份失敗:%1$s - 確認還原應用列表 - 此操作將覆蓋當前嘅應用列表,是否繼續? - 應用列表還原成功 - 應用列表還原失敗:%1$s - 備份應用列表 - 還原應用列表 - 自定義背景 - 選擇一張圖片作為應用背景 - 卡片唔透明度 - Android 版本 - 裝置 - 唔允許授予 %s 超級用戶權限 - 禁用 su 兼容性 - 臨時禁止任何應用程式通過 su 命令獲取 Root 權限(現有嘅 Root 進程唔受影響) - 確定要安裝以下 %1$d 個模組嗎?\n\n%2$s - 更多配置 - SELinux - 強制執行 - 寬容模式 - 簡潔模式 - 開啟後將隱藏冇必要嘅卡片 - 隱藏核心版本號 - 隱藏核心部分嘅 KernelSU 版本號 - 強迫症開關 - 隱藏導航欄上嘅超級用戶數、模組數同 KPM 模組數紅點 - 隱藏 SuSFS 狀態信息 - 隱藏主頁上嘅 SuSFS 狀態信息 - 隱藏鏈接卡片 - 隱藏主頁上嘅鏈接卡片信息 - 隱藏模組標籤行 - 隱藏模組卡片中嘅文件夾名稱同大小標籤 - 主題模式 - 跟隨系統 - 淺色 - 深色 - Manual Hook - 動態顏色 - 使用系統主題嘅動態顏色 - 選擇主題色 - 藍色 - 綠色 - 紫色 - 橙色 - 粉色 - 高級灰 - 黃色 - 刷寫 AnyKernel3 刷寫包 - 刷入 Anykernel3 核心 - 需要 root 權限 - 刷寫完成 - 是否立即重新啟動? - - - 重新啟動失敗 - 核心模組 - 暫冇已安裝嘅核心模組 - 版本 - 作者 - 卸載 - 卸載成功 - 卸載失敗 - 加載 KPM 模組成功 - 加載 KPM 模組失敗 - 參數 - 調參 - KPM 版本 - 關閉 - 以下核心模組功能由 KernelPatch 開發,經過修改後加入 SukiSU Ultra 嘅核心模組功能 - SukiSU Ultra 展望 - 成功 - 錯誤 - SukiSU Ultra 未來將會成為一個相對獨立嘅 KSU 分支,但仲係會感謝官方 KernelSU 同 MKSU 等做出嘅貢獻 - 唔支援 - 支援 - 核心未進行補丁 - 核心未配置 - 個性化配置 - 安裝 - 加載 - 嵌入 - 請選擇: %1\$s 模組嘅安裝模式 \n\n加載:臨時加載模組\n嵌入:永久安裝到系統 - 無法檢查模組文件是否存在 - 主題顏色 - 文件類型唔正確,請選擇 .kpm 文件 - 卸載 - 將卸載以下 KPM 模組:\n%s - 使用雙指縮放圖片,單指拖動調整位置 - 重置 - - 刷寫完成 - - 準備中… - 清理文件中… - 複製文件中… - 提取刷寫工具… - 修補刷寫腳本… - 刷寫核心中… - 刷寫完成 - - 選擇刷寫槽位 - 請選擇要刷寫 boot 嘅目標槽位 - A 槽位 - B 槽位 - 已選擇槽位: %1$s - 獲取原有槽位 - 設定指定槽位 - 恢復默認槽位 - 當前系統默認槽位:%1$s - - 複製失敗 - 未知錯誤 - 刷寫失敗 - - LKM修補/安裝 - 刷寫 AnyKernel3 - 核心版本:%1$s - 使用修補工具:%1$s - 配置 - 應用配置 - 工具 - - 未找到應用 - SELinux 已設定為啟用狀態 - SELinux 已設定為禁用狀態 - SELinux 狀態更改失敗 - 高級配置 - 外觀配置 - 返回 - 背景設定成功 - 已移除自定義背景 - 備選圖標 - 更換為 KernelSU 圖標 - 已切換圖標 - - 隱藏 KPM 功能 - 喺主頁同底欄隱藏 KPM 相關功能同信息 - - 選擇使用嘅 WebUI 引擎 - 自動選擇 - 強制使用 WebUI X - 強制使用 KSU 嘅 WebUI - 將 Eruda 注入 WebUI X - 喺 WebUI X 中注入調試控制台,使調試更容易,需要啟用 WebView 調試 - - 應用 DPI - 僅調整當前應用嘅屏幕顯示密度 - - - - 超大 - 自定義 - 應用 DPI 配置 - 確認更改 DPI - 您確定要將應用 DPI 從 %1$d 更改為 %2$d 嗎? - 應用需要重新啟動以應用新嘅 DPI 配置,唔會影響系統狀態欄或其他應用 - DPI 已設定為 %1$d,重新啟動應用後生效 - - 應用語言 - 跟隨系統 - 卡片暗度調節 - - 錯誤代碼 - 請查看日誌 - 正在安裝模組 %1$d/%2$d - %d 個模組安裝失敗 - 模組下載失敗 - 核心刷寫 - - 全部 - Root - 自定義 - 默認 - - 名稱升序 - 名稱降序 - 安裝時間(新) - 安裝時間(舊) - 大小降序 - 大小升序 - 使用頻率 - - 此分類中冇應用 - - 取消授權 - 授權 - 卸載模組掛載 - 禁用卸載模組掛載 - 展開菜單 - 收起菜單 - 頂部 - 底部 - 已選擇 - 選擇 - - 選項菜單 - 排序方式 - 應用類型選擇 - - SuSFS 配置 - 配置說明 - 此功能允許您自定義 SuSFS 嘅 uname 值同構建時間偽裝。輸入您想要設定嘅值,點擊應用即可生效 - Uname 值 - 請輸入自定義 uname 值 - 構建時間偽裝 - 請輸入構建時間偽裝值 - 當前值: %s - 當前構建時間: %s - 重置為默認值 - 應用 - - 確認重置 - - 無法找到 ksu_susfs 文件 - SuSFS 命令執行失敗 - 執行 SuSFS 命令時出錯: %s - SuSFS 核心名稱同構建時間設定成功: %s, %s - - SuSFS 配置 - - 開機自啟動 - 重新啟動時自動應用所有非默認配置 - 需要添加配置後才能啟用 - 啟用開機自啟動失敗 - 禁用開機自啟動失敗 - 開機自啟動配置錯誤: %s - 冇可用嘅配置進行開機自啟動 - - 基本配置 - SuS 路徑 - SuS 掛載 - 嘗試卸載 - 路徑配置 - 啟用功能狀態 - - 添加 SuS 路徑 - 添加 SuS 掛載 - 嘗試添加卸載 - SuS 路徑添加成功 - 錯誤冇此找到路徑 - 路徑 - 掛載路徑 - 例如: /system/addon.d - 暫冇 SuS 路徑配置 - 暫冇 SuS 掛載配置 - 暫冇嘗試卸載配置 - - 卸載模式 - 普通卸載 (0) - 分離卸載 (1) - 普通 - 分離 - 模式: %1$s (%2$s) - 嘗試 umount 路徑添加成功: %s - 嘗試 umount 路徑存儲成功: %s - - - 重置 SuS 路徑 - 這將清除所有 SuS 路徑配置,確定要繼續嗎? - 重置 SuS 掛載 - 這將清除所有 SuS 掛載配置,確定要繼續嗎? - 重置嘗試卸載 - 這將清除所有嘗試卸載配置,確定要繼續嗎? - 重置路徑配置 - - Android Data 路徑 - SD 卡路徑 - 設定 Android Data 路徑 - 設定 SD 卡路徑 - - 顯示當前 SuSFS 啟用嘅功能狀態 - 未找到功能狀態信息 - 已啟用 - 已禁用 - - SuS 路徑支援 - SuS 掛載支援 - 嘗試卸載支援 - 欺騙 uname 支援 - 欺騙 Cmdline/Bootconfig - 開放重定向支援 - 日誌記錄支援 - 自動默認掛載 - 自動綁定掛載 - 自動嘗試卸載綁定掛載 - 隱藏 KSU SuSFS 符號 - SuS Kstat 支援 - SuS SU 模式切換功能 - - 可配置嘅 SuSFS 功能 - SuSFS 啟用日誌 - 啟用或者關閉 SuSFS 嘅日誌 - SuSFS 日誌配置 - 啟用 SuSFS 日誌 - 關閉 SuSFS 日誌 - 更新配置 - 更新配置地址已複製到剪貼板 - - 顯示更多模組信息 - 顯示額外嘅模組信息,如更新配置 URL 等 - 執行位置 - 當前執行位置:%s - Service - Post-FS-Data - 喺系統服務啟動後執行 - 喺文件系統掛載後但系統完全啟動前執行,可能會導致循環重新啟動 - 槽位信息 - 查看當前啟動槽位信息並複製數值 - 當前活動槽位:%s - Uname:%s - 構建時間:%s - 當前 - 使用 Uname - 使用構建時間 - 無法獲取槽位信息 - - SuSFS 自動開啟模組已啟用,模組路徑:%s - SuSFS 自動開啟模組已停用 - - Kstat 配置 - Kstat 靜態配置已新增:%1$s - 已移除 Kstat 配置:%1$s - Kstat 路徑已新增:%1$s - 已移除 Kstat 路徑:%1$s - Kstat 已更新:%1$s - Kstat 完整複製已更新:%1$s - 新增 Kstat 靜態配置 - 檔案/目錄路徑 - 提示:可以用「default」嚟用返原本嘅值 - 新增 Kstat 路徑 - 新增 - 重設 Kstat 配置 - 你確定要清除晒所有 Kstat 配置咩?呢個動作係唔可以撤銷嘅 - Kstat 配置說明 - • add_sus_kstat_statically:靜態配置檔案/目錄嘅 stat 資訊 - • add_sus_kstat:喺綁定掛載之前新增路徑,儲存原本嘅 stat 資訊 - • update_sus_kstat:更新目標 ino,保持 size 同 blocks 唔變 - • update_sus_kstat_full_clone:淨係更新 ino,其他保持原本嘅值 - 靜態 Kstat 配置 - Kstat 路徑管理 - 暫時冇 Kstat 配置,請撳上面個掣嚟新增配置 - - SuS 掛載隱藏控制 - 控制 SuS 掛載對程序嘅隱藏行為 - 對所有程序隱藏 SuS 掛載 - 啟用之後,SuS 掛載會對所有程序隱藏,包括 KSU 程序 - 禁用之後,SuS 掛載只會對非 KSU 程序隱藏,KSU 程序可以睇到掛載 - 已啟用對所有程序隱藏 SuS 掛載 - 已禁用對所有程序隱藏 SuS 掛載 - 建議喺解鎖螢幕之後或者喺 service.sh 或 boot-completed.sh 階段設為禁用,可以修復一啲依賴 KSU 程序掛載嘅 root 應用問題 - 而家嘅配置: %s - 對所有程序隱藏 - 淨係對非 KS> 程序隱藏 - 核心版本簡潔模式 - 啟用或者禁用 SukiSU 核心版本顯示嘅簡潔模式 - Android Data 路徑已配置為: %s - SD 卡路徑已配置為: %s - 路徑配置可能未完全成功,但會繼續新增 SuS 路徑 - - 備份 - 建立所有 SuSFS 配置嘅備份。備份檔案會包含所有配置、路徑同配置信息。 - 建立備份 - 備份成功建立:%s - 備份建立失敗:%s - 搵唔到備份檔案 - 備份檔案格式無效 - 備份版本唔一樣,但會嘗試還原 - 還原 - 由備份檔案還原 SuSFS 配置。呢個動作會覆蓋晒而家所有配置。 - 揀備份檔案 - 配置還原成功,備份建立於 %s,嚟自裝置:%s - 還原失敗:%s - 確認還原 - 呢個動作會覆蓋晒而家所有 SuSFS 配置。你確定要繼續咩? - 還原 - 備份日期:%s - 裝置:%s - 版本:%s - 隱藏 BL 指令碼 - 啟用隱藏 Bootloader 解鎖狀態指令碼 - 清理工具殘留 - 清理各種模組以及工具嘅殘留檔案同目錄(可能會誤刪導致丟失以及無法啟動,謹慎使用) - 編輯 SuS 路徑 - 編輯 SuS 掛載 - 編輯嘗試解除安裝 - 編輯 Kstat 靜態配置 - 編輯 Kstat 路徑 - 儲存 - 編輯 - 刪除 - 更新 - Kstat 配置更新 - Kstat 路徑更新 - Susfs 完整克隆更新 - 解除安裝 Zygote 隔離服務 - 啟用此選項將喺系統啟動時解除安裝 Zygote 隔離服務掛載點 - Zygote 隔離服務解除安裝已啟用 - Zygote 隔離服務解除安裝已禁用 - 應用路徑 - 其他路徑 - 其他 - 應用 - 添加應用程式路徑 - 搜尋應用程式 - %1$d 個已選應用程式 - %1$d 個已添加應用程式 - 所有应用均已添加 - 動態簽名配置 - 已啟用(大小: %s) - 未啟用 - 啟用動態簽名 - 簽名大小 - 簽名哈希值 - 哈希值必須是 64 位十六進制字符 - 動態簽名配置設定成功 - 動態簽名配置設定失敗 - 無效嘅簽名配置 - 動態簽名已禁用 - 清除動態簽名錯誤 - 動態 - 簽名 %1$d - 未知 - 活躍管理器 - 唔活躍管理器 - Zygisk 實現 - - - 循環路徑配置 - 循環路徑會喺每次非 root 用戶應用程式或者隔離服務啟動時,重新標記做 SUS_PATH。咁樣可以解決因為 inode 狀態重設或者核心重新建立 inode 而令到添加嘅路徑失效嘅問題。 - AVC 日誌欺騙 - AVC 日誌欺騙已經啟用 - AVC 日誌欺騙已經禁用 - 禁用:喺核心 AVC 日誌入面禁用對 \'su\' 嘅 sus tcontext 進行欺騙。\n -啟用:喺核心 AVC 日誌入面將 \'su\' 嘅 sus tcontext 欺騙成 \'kernel\'。 - 重要提示:\n -- 核心入面嘅預設設定係 \'0\'。\n -- 啟用呢個功能有時會令到開發人員喺除錯權限或者 SELinux 問題嗰陣難以搵出原因,所以建議用戶喺除錯時禁用呢個功能。 - - 已驗證 - 模組簽名已驗證 - 驗證簽名 - 模組安裝嗰陣,會強制驗證個簽名。(淨係 ARM架構 用得) - 未知發布者 - 未經簽名嘅模組可能唔完整。為咗保護裝置,已經阻止安裝呢個模組。 - 未經簽名嘅模組可能唔完整。你想唔想安裝嚟自未知發布者嘅模組? - - - - - - - - - + 開啟應用程式時自動檢查更新 + 解除安裝 + 保存日志 + 處理中… + 下拉刷新 + 鬆開刷新 + 正在刷新… + 刷新成功
diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index deac5de4..facfa684 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -1,29 +1,28 @@ 首頁 - 未安裝 - 點擊開始安裝 - 運作中 - 版本:%s - 不支援 - 內核上未偵測到 KernelSU 驅動程式,內核錯誤? - 內核版本 - SuSFS 版本 - 管理器版本 + 尚未安裝 + 點選開始安裝 + 已開始運作 + 版本:%d + 授權:%d 個應用程式 + 未受支援 + KernelSU 目前僅支援 GKI 核心 + 核心版本 + 管理工具 + 指紋資訊 SELinux 狀態 - 已禁用 - 嚴格模式 - 寬鬆模式 - 未知 - 超級使用者 + 已停用 + 強制執行 + 容許執行 + 不明 + 授權 無法啟用模組:%s - 無法禁用模組:%s - 尚未安裝任何模組 + 無法停用模組:%s + 查無已安裝的模組 模組 - 排序(可執行優先) - 排序(已啟用優先) 解除安裝 - 安裝模組 + 安裝 安裝 重新啟動 設定 @@ -33,649 +32,113 @@ 重新啟動至 Download 重新啟動至 EDL 關於 - 確定要解除安裝模組 %s 嗎? - %s 已解除安裝 - 解除安裝失敗:%s + 你是否要解除安裝「%s」模組? + 「%s」已解除安裝 + 無法解除安裝:%s 版本 作者 - 重新整理 - 顯示系統應用程式 - 隱藏系統應用程式 + OverlayFS 已遭核心停用,無法使用模組功能! + 重新載入 + 顯示系統程式 + 隱藏系統程式 傳送日誌 安全模式 - 重新啟動以生效 - 因與 Magisk 衝突,所有模組將無法使用! - 了解 KernelSU + 將在重新啟動時生效 + 與 Magisk 發生衝突,無法使用模組功能! + 掛載:%d 個模組 + 深入瞭解 KernelSU https://kernelsu.org/zh_TW/guide/what-is-kernelsu.html - 了解如何安裝 KernelSU 以及如何開發模組 - 支持開發 - KernelSU 將保持免費開源,向開發者捐款以表支持。 - 加入我們的 %2$s 頻道]]> + 知曉安裝、使用 KernelSU 本體與其模組功能的方法 + 協助發展 + KernelSU 一向以免費與開放原始碼自居,矢志不渝。若想協助我們,可以用小額捐款表達你對專案發展的大力支持。 + 前往 %2$s 加入頻道]]> + 解除掛載模組功能 + 無法更新「%s」App Profile + 管理工具無法以老舊的 KernelSU %d 版本正常運作。請升級至 %d 以上的版本! + 預設解除掛載模組功能 + 將 App Profile 的全域預設行為設作「解除掛載模組功能」。啟用後,將向未指派 Profile 的應用程式移除模組功能。 + 啟用選項後,KernelSU 會將應用程式內遭模組修改的檔案恢復原狀。 預設 - 模板 自訂 - 名稱 - 群組 權限 - SELinux - 卸載模組 - 為 %s 更新應用程式設定檔失敗 - 目前 KernelSU 版本 %s 過低,管理器無法正常運作,請將內核 KernelSU 版本升級至 %s 或以上! - 預設卸載模組 - 應用程式設定檔中\"卸載模組\"\的全域預設值,若啟用,將為未設定設定檔的應用程式移除所有模組對系統的修改。 - 啟用此選項後,將允許 KernelSU 為此應用程式還原被模組修改的檔案。 - 規則 - 更新 正在下載模組:%s - 開始下載:%s - 發現新版本:%s,點擊升級。 - 啟動應用程式 - 強制停止 - 重新啟動應用程式 - 為 %s 更新 SELinux 規則失敗 - 更新日誌 - 應用程式設定檔模板 - 管理本地和線上的應用程式設定檔模板 - 建立模板 - 編輯模板 - 模板 ID - 模板 ID 不合法 + 重新執行 + 範本 + Profile 名稱 + 命名空間掛載 + 繼承 + 全域 + 個體 + 群組 + SELinux 上下文 + 定域 + 更新 + 準備下載模組:%s + 版本 %s 現已開放下載,點選開始更新。 + 開始執行 + 強制停止 + 無法為「%s」更新 SELinux 規則 + 更新說明 + 範本編號無效 + 建立範本 + 編輯範本 + 編號 + App Profile 範本 + 管理 App Profile 的本地、線上範本 + 已成功匯入 + 匯出至剪貼簿 + 查無可供匯出的本地範本! + 編號已由其他範本領有! + 自剪貼簿匯入 + 無法取得更新說明:%s 名稱 - 描述 + 同步線上範本 + 唯讀 + 匯入/匯出 + 無法儲存範本 + 說明 儲存 刪除 - 檢視模板 - 唯讀 - 模板 ID 已存在! - 匯入/匯出 - 從剪貼簿匯入 - 匯出至剪貼簿 - 無可匯出的本地模板! - 匯入成功 - 同步線上規則 - 模板儲存失敗 - 剪貼簿為空! - 獲取更新日誌失敗:%s + 查無剪貼簿內容! + 檢視範本 + 旨在偵錯 WebUI。請依自身狀況適時啟用。 + 啟用 WebView 偵錯 + 無法獲取 Root 權限! + 開啟 檢查更新 - 在應用程式啟動後自動檢查是否有最新版本 - 獲取 root 權限失敗! - 執行 - 關閉 - 啟用 WebView 除錯 - 可用於除錯 WebUI,請僅在需要時啟用。 + 開啟本應用程式時,自動檢查更新 + 選擇檔案 + 安裝至非作用擴充槽(適用於 OTA 更新過後) + 你的裝置將在重新啟動時**強制**開機至目前的非作用擴充槽!\n確保僅在 OTA 更新過後選擇此選項。\n是否繼續? 直接安裝(推薦) - 選擇需要修補的映像檔 - 安裝至未使用的槽位(OTA 後) - 將在重新啟動後強制切換至另一槽位!\n注意:僅能在 OTA 更新完成後重新啟動前使用。\n確定繼續? - 下一步 - 建議選擇 %1$s 分區映像檔 + 繼續 選擇 KMI + 建議選擇 %1$s 分區的映像檔 解除安裝 - 臨時解除安裝 - 永久解除安裝 - 還原原廠映像檔 - 臨時解除安裝 KernelSU,下次重新啟動後恢復至原始狀態。 - 完全且永久解除安裝 KernelSU(含 Root 權限和所有模組)。 - 還原原廠映像檔(若存在備份),通常在 OTA 前使用;若需解除安裝 KernelSU,請使用\"永久解除安裝\"。 - 正在刷寫 - 刷寫完成 - 刷寫失敗 - 已選擇的 LKM:%s + 暫時性解除安裝 + 恢復原廠映像檔 + 暫時解除安裝 KernelSU。會在下次重新啟動時恢復原狀。 + 永久性解除安裝 + 徹底解除安裝 KernelSU(含 Root 授權與所有的模組)。 + 正在閃刷 + 閃刷成功 + 若裝置內含有備份檔案,遂以 OTA 更新前的原廠系統映像檔進行復原。若需要解除安裝 KernelSU,請選擇「永久性解除安裝」。 + 閃刷失敗 + 已選定 LKM:%s 儲存日誌 - 日誌已儲存 - - 確定安裝模組 %1$s? - 未知模組 - - 確定還原模組 - 此操作將覆蓋所有現有模組,是否繼續? - 確定 - 取消 - - 備份成功 (tar.gz) - 備份失敗:%1$s - 備份模組 - 還原模組 - - 模組已成功還原,需重新啟動生效 - 還原失敗:%1$s - 立即重新啟動 - 未知錯誤 - - 命令執行失敗:%1$s - - 應用程式清單備份成功 - 應用程式清單備份失敗:%1$s - 確定還原應用程式清單 - 此操作將覆蓋目前的應用程式清單,是否繼續? - 應用程式清單還原成功 - 應用程式清單還原失敗:%1$s - 備份應用程式清單 - 還原應用程式清單 - 自訂背景 - 選擇一張圖片作為應用程式背景 - 卡片不透明度 - Android 版本 - 設備型號 - 不允許授予 %s 超級使用者權限 - 禁用 su 相容性 - 暫時禁止任何應用程式透過 su 命令取得 Root 權限(現有的 Root 程序不受影響) - 確定要安裝以下 %1$d 個模組嗎?\n\n%2$s - 更多設定 - SELinux - 嚴格模式 - 寬鬆模式 - 簡潔模式 - 啟用後將隱藏不必要的卡片 - 隱藏內核版本號 - 隱藏內核部分的 KernelSU 版本號 - 強迫症開關 - 隱藏首頁上的超級使用者數量、模組數量及 KPM 模組數量資訊 - 隱藏 SuSFS 狀態資訊 - 隱藏首頁上的 SuSFS 狀態資訊 - 隱藏 Zygisk 狀態資訊 - 隱藏主頁上的 Zygisk 實現狀態資訊 - 隱藏連結卡片 - 隱藏首頁上的連結卡片資訊 - 隱藏模組標籤行 - 隱藏模組卡片中的資料夾名稱和大小標籤 - 主題模式 - 跟隨系統 - 淺色 - 深色 - 手動掛載 - 動態顏色 - 使用系統主題的動態顏色 - 選擇主題顏色 - 藍色 - 綠色 - 紫色 - 橙色 - 粉色 - 高級灰 - 黃色 - 刷寫 AnyKernel3 壓縮包 - 刷寫 AnyKernel3 內核 - 需要 Root 權限 - 刷寫完成 - 是否立即重新啟動? - - - 重新啟動失敗 - 內核模組 - 暫無已安裝的內核模組 - 版本 - 作者 - 解除安裝 - 解除安裝成功 - 解除安裝失敗 - 載入 KPM 模組成功 - 載入 KPM 模組失敗 - 參數 - 調參 - KPM 版本 - 關閉 - 以下內核模組功能由 KernelPatch 開發,經修改後加入 SukiSU Ultra 的內核模組功能 - SukiSU Ultra 展望 - 成功 - 錯誤 - SukiSU Ultra 未來將成為一個相對獨立的 KSU 分支,但仍感謝官方 KernelSU 及 MKSU 等做出的貢獻 - 不支援 - 支援 - 內核未進行修補 - 內核未配置 - 個人化設定 - 安裝模式 - 載入 - 嵌入 - 請選擇 %1$s 模組的安裝模式\n\n載入:臨時載入模組\n嵌入:永久安裝至系統 - 無法檢查模組檔案是否存在 - 主題顏色 - 檔案類型不正確,請選擇 .kpm 檔案 - 解除安裝 - 將解除安裝以下 KPM 模組:\n%s - 使用雙指縮放圖片,單指拖曳調整位置 - 重置 - - 刷寫完成 - - 準備中… - 清理檔案… - 複製檔案… - 提取刷寫工具… - 修補刷寫腳本… - 正在刷寫內核… - 刷寫完成 - - 選擇刷寫槽位 - 請選擇要刷寫 boot 的目標槽位 - A 槽位 - B 槽位 - 已選擇槽位:%1$s - 取得原有槽位 - 設定指定槽位 - 恢復預設槽位 - 目前槽位:%1$s - - 複製失敗 - 未知錯誤 - 刷寫失敗 - - LKM 修補/安裝 - 刷寫 AnyKernel3 - 內核版本:%1$s - 使用修補工具:%1$s - 配置 - 應用程式設定 - 工具 - - 未找到應用程式 - SELinux 已設為啟用狀態 - SELinux 已設為禁用狀態 - SELinux 狀態更改失敗 - 進階設定 - 外觀設定 - 返回 - 背景設定成功 - 已移除自訂背景 - 備用圖示 - 更換為 KernelSU 圖示 - 已切換圖示 - - 隱藏 KPM 功能 - 在首頁和底欄隱藏 KPM 相關功能與資訊 - - 選擇使用的 WebUI 引擎 - 自動選擇 - 強制使用 WebUI X - 強制使用 KSU 的 WebUI - 將 Eruda 注入 WebUI X - 在 WebUI X 中注入除錯控制台,方便除錯,需啟用 WebView 除錯 - - 應用程式 DPI - 僅調整目前應用程式的螢幕顯示密度 - - - - 超大 - 自訂 - 套用 DPI 設定 - 確認更改 DPI - 您確定要將應用程式 DPI 從 %1$d 更改為 %2$d 嗎? - 應用程式需重新啟動以套用新的 DPI 設定,不會影響系統狀態列或其他應用程式 - DPI 已設為 %1$d,重新啟動應用程式後生效 - - 應用程式語言 - 跟隨系統 - 卡片暗度調整 - - 錯誤代碼 - 請檢查日誌 - 正在安裝模組 %1$d/%2$d - %d 個模組安裝失敗 - 模組下載失敗 - 內核刷寫 - - 全部 - Root - 自訂 - 預設 - - 名稱升序 - 名稱降序 - 安裝時間(新) - 安裝時間(舊) - 大小降序 - 大小升序 - 使用頻率 - - 此分類中無應用 - - 取消授權 - 授權 - 卸載模組掛載 - 停用卸載模組掛載 - 展開選單 - 收合選單 - 頂部 - 底部 - 已選取 - 選取 - - 選單選項 - 排序方式 - 應用程式類型選擇 - - SuSFS 配置 - 配置說明 - 此功能允許您自訂 SuSFS 的 uname 值和構建時間偽裝。輸入您想要設定的值,點選應用即可生效 - Uname 值 - 請輸入自訂 uname 值 - 構建時間偽裝 - 請輸入構建時間偽裝值 - 目前值: %s - 目前構建時間: %s - 重設為預設值 - 應用 - - 確認重設 - - 無法找到 ksu_susfs 檔案 - SuSFS 指令執行失敗 - 執行 SuSFS 指令時出錯: %s - SuSFS 內核名称和构建时间设置成功: %s, %s - - SuSFS 配置 - - 開機自啟動 - 系統啟動時自動應用所有非預設配置 - 需要添加配置後才能啟用 - 開啟開機自動啟動失敗 - 禁用開機自動啟動失敗 - 開機自動啟動設定錯誤:%s - 無可用設定進行開機自動啟動 - - 基本設定 - SuS 路徑 - SuS 掛載 - 嘗試卸載 - 路徑設定 - 啟用功能狀態 - - 新增 SuS 路徑 - 新增 SuS 掛載 - 新增嘗試卸載 - 成功添加 SuS 路径 - 未找到路径 - 路徑 - 掛載路徑 - 例如:/system/addon.d - 暫無 SuS 路徑設定 - 暫無 SuS 掛載設定 - 暫無嘗試卸載設定 - - 卸載模式 - 一般卸載 (0) - 分離卸載 (1) - 一般 - 分離 - 模式:%1$s (%2$s) - 嘗試 umount 路徑新增成功: %s - 嘗試 umount 路徑儲存成功: %s - - - 重設 SuS 路徑 - 這將清除所有 SuS 路徑設定,確定要繼續嗎? - 重設 SuS 掛載 - 這將清除所有 SuS 掛載設定,確定要繼續嗎? - 重設嘗試卸載 - 這將清除所有嘗試卸載設定,確定要繼續嗎? - 重置路徑設定 - - Android Data 路徑 - SD 卡路徑 - 設定 Android Data 路徑 - 設定 SD 卡路徑 - - 顯示目前 SuSFS 啟用的功能狀態 - 未找到功能狀態資訊 - 已啟用 - 已停用 - - SuS 路徑支援 - SuS 掛載支援 - 嘗試卸載支援 - 偽裝 uname 支援 - 偽裝 Cmdline/Bootconfig - 開放重定向支援 - 日誌記錄支援 - 自動預設掛載 - 自動綁定掛載 - 自動嘗試卸載綁定掛載 - 隱藏 KSU SuSFS 符號 - SuS 內核統計支援 - SuS SU 模式切換功能 - - 可設定的 SuSFS 功能 - SuSFS 啟用日誌 - 啟用或關閉 SuSFS 的日誌 - SuSFS 日誌設定 - 啟用 SuSFS 日誌 - 關閉 SuSFS 日誌 - 更新設定 - 更新設定位址已複製到剪貼簿 - - 顯示更多模組資訊 - 顯示額外的模組資訊,如更新設定 URL 等 - 執行位置 - 目前執行位置:%s - 服務 - 檔案系統掛載後 - 在系統服務啟動後執行 - 在檔案系統掛載後但系統完全啟動前執行,可能導致循環重新啟動 - 槽位資訊 - 檢視目前啟動槽位資訊並複製數值 - 目前活動槽位:%s - Uname:%s - 建置時間:%s - 目前 - 使用 Uname - 使用建置時間 - 無法取得槽位資訊 - - SuSFS 自啟動模組已啟用,模組路徑:%s - SuSFS 自啟動模組已停用 - - Kstat 設定 - Kstat 靜態設定已新增:%1$s - 已移除 Kstat 設定:%1$s - Kstat 路徑已新增:%1$s - 已移除 Kstat 路徑:%1$s - Kstat 已更新:%1$s - Kstat 完整複製已更新:%1$s - 新增 Kstat 靜態設定 - 檔案/目錄路徑 - 提示:可使用「default」來使用原始值 - 新增 Kstat 路徑 - 新增 - 重置 Kstat 設定 - 確定要清除所有 Kstat 設定嗎?此操作不可撤銷 - Kstat 設定說明 - • add_sus_kstat_statically:靜態設定檔案/目錄的 stat 資訊 - • add_sus_kstat:在綁定掛載前新增路徑,儲存原始 stat 資訊 - • update_sus_kstat:更新目標 ino,保持 size 和 blocks 不變 - • update_sus_kstat_full_clone:僅更新 ino,其他保持原始值 - 靜態 Kstat 設定 - Kstat 路徑管理 - 暫無 Kstat 設定,點擊下方按鈕新增設定 - - SuS 掛載隱藏控制 - 控制 SuS 掛載對程序的隱藏行為 - 對所有程序隱藏 SuS 掛載 - 啟用後,SuS 掛載將對所有程序隱藏,包含 KSU 程序 - 停用後,SuS 掛載僅對非 KSU 程序隱藏,KSU 程序可以看到掛載 - 已啟用對所有程序隱藏 SuS 掛載 - 已停用對所有程序隱藏 SuS 掛載 - 建議在螢幕解鎖後或在 service.sh 或 boot-completed.sh 階段設定為停用,這可以修復一些依賴 KSU 程序掛載的 root 應用程式問題 - 目前設定: %s - 對所有程序隱藏 - 僅對非 KSU 程序隱藏 - 內核版本簡潔模式 - 啟用或停用 SukiSU 內核版本顯示的簡潔模式 - Android Data 路徑已設定為: %s - SD 卡路徑已設定為: %s - 路徑設定可能未完全成功,但將繼續新增 SuS 路徑 - - 備份 - 建立所有 SuSFS 配置的備份。備份檔案將包含所有設定、路徑和配置資訊。 - 建立備份 - 備份建立成功:%s - 備份建立失敗:%s - 備份檔案未找到 - 無效的備份檔案格式 - 備份版本不符,但將嘗試還原 - 還原 - 從備份檔案還原 SuSFS 配置。這將覆蓋所有當前設定。 - 選擇備份檔案 - 配置還原成功,備份創建於 %s,來自裝置:%s - 還原失敗:%s - 確認還原 - 這將覆蓋所有當前的 SuSFS 配置。您確定要繼續嗎? - 還原 - 備份日期:%s - 裝置:%s - 版本:%s - 上鎖狀態 - 覆蓋引導鎖狀態屬性於 late_start 服務模式 - 清理工具殘留 - 清理各種模組以及工具的殘留檔案和目錄(可能會誤刪導致丟失以及無法啟動,謹慎使用) - 編輯 SuS 路徑 - 編輯 SuS 掛載 - 編輯嘗試解除安裝 - 編輯 Kstat 靜態配置 - 編輯 Kstat 路徑 - 儲存 - 編輯 - 刪除 - 更新 - Kstat 配置已更新 - Kstat 路徑已更新 - Susfs 完整複製更新 - 卸載 Zygote 隔離服務 - 啟用此選項將在系統啟動時卸載 Zygote 隔離服務掛載點 - Zygote 隔離服務卸載已啟用 - Zygote 隔離服務卸載已停用 - 應用路徑 - 其他路徑 - 其他 - 應用程式 - 新增應用路徑 - 搜尋應用程式 - %1$d 個已選應用 - %1$d 個已新增應用 - 所有應用均已新增 - 動態管理器設定 - 已啟用(大小: %s) - 未啟用 - 啟用動態管理器 - 動態管理器簽名大小 - 動態管理器簽名哈希值 - 哈希值必須是 64 位十六進位字元 - 動態管理器設定成功 - 動態管理器設定失敗 - 無效的簽名設定 - 動態管理器已停用 - 清除動態管理器錯誤 - 動態 - 簽名 %1$d - 未知 - 活躍管理器 - 無活躍管理器 - Zygisk 實現 - - SuS 循環路徑 - 新增 SuS 循環路徑 - 編輯 SuS 循環路徑 - SuS 循環路徑新增成功: %1$s - SuS 循環路徑已移除: %1$s - SuS 循環路徑已更新: %1$s -> %2$s - 未設定 SuS 循環路徑 - 重設循環路徑 - 確定要清空所有 SuS 循環路徑嗎?此操作無法撤銷。 - 循環路徑 - 注意:只有不在 /storage/ 和 /sdcard/ 內的路徑才能透過循環路徑新增。 - 錯誤:循環路徑不能位於 /storage/ 或 /sdcard/ 目錄內 - 循環路徑 - 新增循環路徑 - - 循環路徑設定 - 循環路徑會在每次非 root 使用者應用程式或隔離服務啟動時重新標記為 SUS_PATH。這有助於解決新增的路徑可能因 inode 狀態重設或內核中 inode 重新建立而失效的問題 - AVC 日誌偽裝 - AVC 日誌偽裝已啟用 - AVC 日誌偽裝已停用 - 停用: 停用在內核 AVC 日誌中偽裝 \'su\' 的 sus tcontext。\n - 啟用: 啟用在內核 AVC 日誌中將 \'su\' 的 sus tcontext 偽裝為 \'kernel\' - 重要提示:\n -- 內核中預設設定為 \'0\'\n -- 啟用此功能有時會使開發人員在除錯許可權或 SELinux 問題時難以識別原因,因此建議使用者在除錯時禁用此功能。 - - 已驗證 - 模組簽名已驗證 - 驗證簽名 - 模組安裝時,強制驗證簽名。 (僅 ARM 架構可用) - 未知發布者 - 未經簽名的模組可能不完整。為了對設備進行保護,已阻止安裝此模組。 - 未經簽名的模組可能不完整。你想安裝來自未知發布者的模組嗎? - 鉤子類型 - - KPM 修補 - 用於新增附加的 KPM 功能 - KPM 修補 - 在刷機前對內核映象進行 KPM 修補 - KPM 撤銷修補 - 撤銷之前套用的 KPM 修補 - KPM 修補已啟用 - KPM 撤銷修補已啟用 - KPM 修補模式 - KPM 撤銷修補模式 - - 準備 KPM 修補工具 - 正在應用 KPM 修補 - 正在撤銷 KPM 修補 - 找到 Image 檔案: %s - KPM 修補成功 - KPM 撤銷修補成功 - 檔案重新打包完成 - - 解壓壓縮檔失敗 - 未找到 Image 檔案 - KPM 修補失敗 - KPM 撤銷修補失敗 - KPM 修補操作失敗: %s - - 跟隨內核 - 原樣使用內核,不進行任何 KPM 修改 - - 使用者態掃描應用列表 - 開啟後將使用使用者態掃描應用列表,提高穩定性 (因內核掃描應用列表出現卡死等問題可以嘗試開啟此選項) - 多使用者應用掃描 - 開啟後將掃描所有使用者的應用,包括工作資料等 - 設定失敗,請檢查許可權 - 清理執行環境 - 清理執行時檔案並停止掃描服務 - 您確定要清理執行環境嗎?這將停止掃描服務並刪除相關檔案 - 執行環境清理成功 - 執行環境清理失敗 - - 確認安裝 - 確認安裝 (%d 個檔案) - 確認安裝 - 模組 - 核心 - 未知型別 - 未知內核 - 未知檔案 - 版本 - 作者 - 描述 - 支援的裝置 - - SUS 映射 - 庫檔案路徑 - 新增 SUS 映射 - 編輯 SUS 映射 - SUS 映射新增成功: %1$s - SUS 映射已移除: %1$s - SUS 映射已更新: %1$s -> %2$s - 未設定 SUS 映射 - 重設 SUS 映射 - 這將移除所有已設定的 SUS 映射。此操作無法復原。 - 記憶體映射隱藏 - 隱藏 /proc/self/ 中各種映射裡的 mmap 真實檔案 - 從 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隱藏記憶體映射的真實檔案路徑。請注意:此功能不支援隱藏匿名記憶體映射,也無法隱藏由注入庫本身產生的內聯勾子 (Inline Hooks) 或 PLT 勾子 (PLT Hooks)。 - 重要提示:對於具備完善注入偵測機制的應用程式,此功能可能無法有效繞過偵測。 - 首先透過 ps -enf 尋找目標應用程式的 PID 和 UID,然後檢查 /proc/<pid>/maps 中的相關路徑,並與 /proc/1/mountinfo 中的裝置號碼進行比對以確保一致性。只有當裝置號碼一致時,隱藏映射才能正常運作。 - - + 執行 + 啟用優先 + 執行優先 + 將安裝以下模組:%1$s + 確認 + 暫時禁用任何應用程式通過⁠ su 命令獲得 root 權限的能力(現有的 root 進程不會受到影響)。 + 無法授予「%s」超級使用者存取 + 停用 su 相容性 + 已儲存運作日誌 + 處理中… + 下拉刷新 + 鬆開刷新 + 正在刷新… + 刷新成功 diff --git a/manager/app/src/main/res/values/colors.xml b/manager/app/src/main/res/values/colors.xml index b8d4746a..a5b623aa 100644 --- a/manager/app/src/main/res/values/colors.xml +++ b/manager/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ - #F2F2F2 + #FFFFFFFF \ No newline at end of file diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index b06e5801..49810248 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -1,755 +1,163 @@ - - - SukiSU Ultra - Home - Not installed - Click to install - Working - Version: %s - Unsupported - No KernelSU driver detected on your kernel, wrong kernel? - Kernel version - SuSFS Version - Manager version - SELinux status - Disabled - Enforcing - Permissive - Unknown - Superuser - Failed to enable module: %s - Failed to disable module: %s - No module installed - Module - Sort (Action first) - Sort (Enabled first) - Uninstall - Install - Install - Reboot - Settings - Soft Reboot - Reboot to Recovery - Reboot to Bootloader - Reboot to Download - Reboot to EDL - About - Are you sure you want to uninstall module %s? - %s uninstalled - Failed to uninstall: %s - Version - Author - Refresh - Show system apps - Hide system apps - Send logs - Safe mode - Reboot to take effect - Modules are unavailable due to a conflict with Magisk! - Learn KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Learn how to install KernelSU and use modules - Support Us - KernelSU is, and always will be, free, and open source. You can however show us that you care by making a donation - Join our %2$s channel

The images of the files with anime character sticker are copyrighted by %3$s, the Brand Intellectual Property in the images is owned by %4$s. Before using these files, in addition to complying with %5$s, you also need to comply with the authorization of the two authors to use these artistic contents]]>
- App Profile - Default - Template - Custom - Profile name - Groups - Capabilities - SELinux context - Umount modules - Failed to update App Profile for %s - The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! - Umount modules by default - The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set - Enabling this option will allow KernelSU to restore any modified files by the modules for this app - Domain - Rules - Update - Downloading module: %s - Start downloading: %s - New version %s is available, click to upgrade - Launch - Force stop - Restart - Failed to update SELinux rules for %s - Changelog - App Profile Template - Manage local and online template of App Profile - Create template - Edit template - ID - Invalid template ID - Name - Description - Save - Delete - View template - Read only - Template ID already exists! - Import/Export - Import from clipboard - Export to clipboard - Cannot find local template to export! - Imported successfully - Sync online templates - Failed to save template - Clipboard is empty! - Fetch changelog failed: %s - Check for updates - Automatically check for updates when opening the app - Failed to grant root! - Action - Close - Enable WebView debugging - Can be used to debug WebUI. Please enable only when needed - Direct install (Recommended) - Select a image that needs to be patched - Install to inactive slot (After OTA) - Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? - Next - Select partition - Use local LKM file - Only .ko files are supported - %1$s partition image is recommended - Select KMI - Uninstall - Uninstall temporarily - Uninstall permanently - Restore stock image - Temporarily uninstall KernelSU, restore to original state after next reboot - Uninstalling KernelSU (Root and all modules) completely and permanently - Restore the stock factory image (If a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\" - Flashing - Flash success - Flash failed - Selected LKM: %s - Save logs - Logs saved - - Confirm install module %1$s? - Unknown module - - Confirm Module Restoration - This operation will overwrite all existing modules. Continue? - Confirm - Cancel - - Backup successful (tar.gz) - Backup failed: %1$s - Backup modules - Restore modules - - Modules restored successfully, restart required - Restore failed: %1$s - Restart Now - Unknown error - - Command execution failed: %1$s - - Allowlist backup successful - Allowlist backup failed: %1$s - Confirm Allowlist Restoration - This operation will overwrite the current allowlist. Continue? - Allowlist restored successfully - Allowlist restore failed: %1$s - Backup Allowlist - Restore Allowlist - Custom App Background - Select an image as background - Navigation bar transparency - Android version - Device model - Granting superuser to %s is not allowed - Disable su compatibility - Disable the ability of any app to gain root privileges via the ⁠su command (existing root processes won\'t be affected). - Disable kernel umount - Disable kernel-level umount behavior controlled by KernelSU. - Enable enhanced security - Enable stricter security policies. - Default - Temporarily enable - Permanently enable - Sure you want to install the following %1$d modules? \n\n%2$s - More settings - SELinux - Enabled - Disabled - Simplicity mode - Hides unnecessary cards when turned on - Hide kernel version - Hide kernel version - Hide other info - Hides Red dot about the number of super users, modules and KPM modules on the navigation bar page - Hide SuSFS status - Hide SuSFS status information on the home page - Hide Zygisk status - Hide Zygisk implementation information on the home page - Hide Link Card Status - Hide link card information on the home page - Hide module label rows - Hide folder name and size labels in module cards - Theme - Follow system - Light - Dark - Manual Hook - Inline Hook - Dynamic colours - Dynamic colours using system themes - Choose a theme colour - Blue - Green - Purple - Orange - Pink - Gray - Yellow - Install Anykernel3 - Flash AnyKernel3 kernel file - Requires root privileges - Scrubbing complete - Whether to reboot immediately? - Yes - No - Reboot Failed - KPM - No installed kernel modules at this time - Version - Author - Uninstall - Uninstalled successfully - Failed to uninstall - Load of kpm module successful - Load of kpm module failed - Parameters - Execute - KPM Version - Close - The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra - SukiSU Ultra Look forward to - Success - Failed - SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions! - Unsupported - Supported - Kernel not patched - Kernel not configured - Custom settings - KPM Install - Load - Embed - Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system - Unable to check if module file exists - Theme Color - Incorrect file type! Please select .kpm file - Uninstall - The following KPM will be uninstalled: %s - Use two fingers to zoom the image, and one finger to drag it to adjust the position - Reprovision - - Flash Complete - - Preparing… - Cleaning files… - Copying files… - Extracting flash tool… - Patching flash script… - Flashing kernel… - Flash completed - - Select Flash Slot - Please select the target slot for flashing boot - Slot A - Slot B - Selected slot: %1$s - Getting the original slot - Setting the specified slot - Restore Default Slot - Current system default slot:%1$s - - Copy failed - Unknown error - Flash failed - - LKM repair/installation - Flashing AnyKernel3 - Kernel version:%1$s - Using the patching tool:%1$s - Configure - Application Settings - Tools - - Application not found - SELinux Enabled - SELinux Disabled - SELinux Status change failed - Advanced Settings - Customize the toolbar - Comeback - Background set successfully - Removed custom backgrounds - Alternate icon - Change the launcher icon to KernelSU\'s icon - Icon switched - - Hides KPM Function - Hides KPM information and Function in home and bottom bar - - Select the WebUI engine to use - Automatic Selection - Force the use of WebUI X - Mandatory use of KSU WebUI - Inject Eruda into WebUI X - Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on - - Applied DPI - Adjust the screen display density for the current application only - Small - Medium - Big - Oversize - Customizable - Applying DPI settings - Confirm DPI change - Are you sure you want to change the application DPI from %1$d to %2$d? - Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications - DPI has been set to %1$d, effective after restarting the application - - App Language - Follow System - Card Darkness Adjustment - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing - - All - Root - Custom - Default - - Ascending order of name - Name descending - Installation time (New) - Installation time (Old) - Descending order of size - Ascending order of size - Frequency of use - - No application in this category - - Delegation of authority - Authorizations - Unmounting Module Mounts - Disable uninstall module mounting - Expand menu - Put away the menu - Top - Bottom - Selected - Select - - Menu Options - Sort by - Application Type Selection - - SuSFS Configuration - Configuration Description - This feature allows you to customize the SuSFS uname value and build time spoofing. Enter the values you want to set and click Apply to take effect - Uname Value - Please enter custom uname value - Build Time Spoofing - Please enter build time spoofing value - Current value: %s - Current build time: %s - Reset to Default - Apply - - Confirm Reset - - Cannot find ksu_susfs file - SuSFS command execution failed - Error executing SuSFS command: %s - SuSFS uname and build time set successfully: %s, %s - - SuSFS Configuration - - Auto Start - Automatically apply all non-default configurations on reboot - Configuration needs to be added to enable - Failed to enable auto start - Failed to disable auto start - Auto start configuration error: %s - No available configuration for auto start - - Basic Settings - SUS Paths - SUS Mounts - Try Umount - Path Settings - Enabled Features Status - - Add SUS Path - Add SUS Mount - Add Try Umount - SUS path added successfully - Path not found error - Path - Mount Path - e.g.: /system/addon.d - No SUS paths configured - No SUS mounts configured - No try umount configured - - Umount Mode - Normal Umount (0) - Detach Umount (1) - Normal - Detach - Mode: %1$s (%2$s) - Try to umount path added successfully: %s - Attempted umount path save succeeded: %s - - - Reset SUS Paths - This will clear all SUS path configurations. Are you sure you want to continue? - Reset SUS Mounts - This will clear all SUS mount configurations. Are you sure you want to continue? - Reset Try Umount - This will clear all try umount configurations. Are you sure you want to continue? - Reset Path Settings - - Android Data Path - SD Card Path - Set Android Data Path - Set SD Card Path - - Display current SuSFS enabled features status - No feature status information found - Enabled - Disabled - - SUS Path Support - SUS Mount Support - Try Umount Support - Spoof uname Support - Spoof Cmdline/Bootconfig - Open Redirect Support - Logging Support - Auto Default Mount - Auto Bind Mount - Auto Try Umount Bind Mount - Hide KSU SUSFS Symbols - SUS Kstat Support - SUS SU mode switching function - - Configurable SuSFS Features - SuSFS Enable Log - Enable or disable logging for SuSFS - SuSFS Logging Configuration - Enabling SuSFS Logging - Turn off SuSFS logging - Update JSON - Update JSON URL copied to clipboard - - Show More Module Info - Display additional module information like update JSON URLs - Execution Location - Current execution location: %s - Service - Post-FS-Data - Execute after system services start - Execute after file system is mounted but before system is fully booted,May cause a boot loop - Slot Information - View current boot slot information and copy values - Current Active Slot: %s - Uname: %s - Build Time: %s - Current - Use Uname - Use Build Time - Unable to retrieve slot information - - SuSFS auto-start module enabled, module path: %s - SuSFS auto-start module disabled - - Kstat Configuration - Kstat static configuration added: %1$s - Kstat configuration removed: %1$s - Kstat path added: %1$s - Kstat path removed: %1$s - Kstat updated: %1$s - Kstat full clone updated: %1$s - Add Kstat Static Configuration - File/Directory Path - Hint: You can use "default" to use the original value - Add Kstat Path - Add - Reset Kstat Configuration - Are you sure you want to clear all Kstat configurations? This action cannot be undone - Kstat Configuration Description - • add_sus_kstat_statically: Static stat info of files/directories - • add_sus_kstat: Add path before bind mount, storing original stat info - • update_sus_kstat: Update target ino, keep size and blocks unchanged - • update_sus_kstat_full_clone: Update ino only, keep other original values - Static Kstat Configuration - Kstat Path Management - No Kstat configuration yet, click the button below to add - - SUS Mount Hiding Control - Control the hiding behavior of SUS mounts for processes - Hide SUS mounts for all processes - When enabled, SUS mounts will be hidden from all processes, including KSU processes - When disabled, SUS mounts will only be hidden from non-KSU processes, KSU processes can see the mounts - Enabled hiding SUS mounts for all processes - Disabled hiding SUS mounts for all processes - It is recommended to set to disabled after screen is unlocked, or during service.sh or boot-completed.sh stage, as this should fix the issue on some rooted apps that rely on mounts mounted by KSU process - Current setting: %s - Hide for all processes - Hide only for non-KSU processes - Kernel Version Concise Mode - Enable or disable the clean mode displayed by the SukiSU kernel version - Android Data path has been set to: %s - SD card path has been set to: %s - Path setup may not be fully successful, but SUS paths will continue to be added - - Backup - Create a backup of all SuSFS configurations. The backup file will include all settings, paths, and configurations - Create Backup - Backup created successfully: %s - Backup creation failed: %s - Backup file not found - Invalid backup file format - Backup version mismatch, but will attempt to restore - Restore - Restore SuSFS configurations from a backup file. This will overwrite all current settings - Select Backup File - Configuration restored successfully from backup created on %s from device: %s - Restore failed: %s - Confirm Restore - This will overwrite all current SuSFS configurations. Are you sure you want to continue? - Restore - Backup Date: %s - Device: %s - Version: %s - Lock state - Overwrite bootloader locking status attribute in late_start service mode - Cleanup Residue - Clean up the residual files and directories of various modules and tools (May be deleted by mistake, resulting in loss and failure to start, use with caution) - Edit SUS Path - Edit SUS Mount - Edit Try Umount - Edit Kstat Static Configuration - Edit Kstat Path - Save - Edit - Delete - Update - Kstat config update - Kstat path update - Susfs update full clone - Unmount Zygote Isolation Service - Enable this option to unmount Zygote isolation service mount points at system startup - Zygote isolation service unmount enabled - Zygote isolation service unmount disabled - Application Path - Other paths - Other - App - Add App Path - SuSFS library version mismatch, kernel: %1$s vs manager: %2$s, It is recommended to update the kernel or manager - Warning - Search Apps - %1$d apps selected - %1$d apps already added - All apps have been added - Dynamic Manager Configuration - Enabled (Size: %s) - Disabled - Enable Dynamic Manager - Dynamic Manager Signature Size - Dynamic Manager Signature Hash - Hash must be 64 hexadecimal characters - Dynamic Manager configuration set successfully - Failed to set dynamic Manager configuration - Invalid Manager configuration - Dynamic Manager disabled - Failed to clear dynamic Manager - Dynamic - Signature %1$d - Unknown - Active Manager - No active manager - SukiSU - Zygisk implement - - SUS Loop Paths - Add SUS Loop Path - Edit SUS Loop Path - SUS loop path added successfully: %1$s - SUS loop path removed: %1$s - SUS loop path updated: %1$s -> %2$s - No SUS loop paths configured - Reset Loop Paths - Are you sure you want to clear all SUS loop paths? This action cannot be undone - Loop Path - /data/example/path - Note: Only paths NOT inside /storage/ and /sdcard/ can be added via loop paths - Error: Loop paths cannot be inside /storage/ or /sdcard/ directories - Loop Paths - Add Loop Path - - Loop Path Configuration - Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel - AVC Log Spoofing - AVC log spoofing has been enabled - AVC log spoofing has been disabled - -Disabled: Disable spoofing the sus tcontext of \'su\' shown in avc log in kernel\n -Enabled: Enable spoofing the sus tcontext of \'su\' with \'kernel\' shown in avc log in kernel - - -Important Note:\n -- It is set to \'0\' by default in kernel\n -- Enabling this will sometimes make developers hard to identify the cause when they are debugging with some permission or SELinux issue, so users are advised to disable this when doing - - - Validated - Module signature verified - Signature Verification - Force signature verification when installing modules (Only available for ARM architecture) - Unknown publisher - Unsigned modules may be incomplete. To protect your device, installation of this module has been blocked - Unsigned modules may be incomplete. Do you want to allow the following module from an unknown publisher to install in this device? - Hook type - - KPM Patch - For adding additional KPM features - KPM Patch - Apply KPM patch to kernel image before flashing - KPM Undo Patch - Undo previously applied KPM patch - KPM patch enabled - KPM undo patch enabled - KPM Patch Mode - KPM Undo Patch Mode - - Preparing KPM tools - Applying KPM patch - Undoing KPM patch - Found Image file: %s - KPM patch applied successfully - KPM patch undone successfully - File repacked successfully - - Failed to extract zip file - Image file not found - KPM patch failed - KPM undo patch failed - KPM patch operation failed: %s - - Follow Kernel - Use kernel as-is without any KPM modifications - - User-mode scanning application list - Enabling this option will use user-mode scanning for the application list, improving stability. (If you encounter issues such as freezing during kernel scanning of the application list, you may try enabling this option.) - Multi-User Application Scanning - When enabled, scans applications for all users, including work profiles - Setting failed, please check permissions - Clean Runtime Environment - Clean up runtime files and stop the scanner service - Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files. - Runtime environment cleaned successfully - Failed to clean runtime environment - - Confirm Installation - Confirm Installation (%d files) - Install - Module - Kernel - Unknown - Unknown Kernel - Unknown File - Version - Author - Description - Supported Devices - - SUS Maps - Library Path - /data/adb/modules/my_module/zygisk/arm64-v8a.so - Add SUS Map - Edit SUS Map - SUS map added successfully: %1$s - SUS map removed: %1$s - SUS map updated: %1$s -> %2$s - No SUS maps configured - Reset SUS Maps - This will remove all configured SUS maps. This action cannot be undone. - Memory Map Hiding - Hide the mmapped real file from various maps in /proc/self/ - Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself. - Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection. - First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly. - - Log Viewer - Back - Search - Clear Logs - Are you sure you want to clear the selected log file? This action cannot be undone. - Logs cleared successfully - Filter by Type - All Types - Showing %1$d of %2$d entries - No logs found - No matching logs found - Refresh - Raw Log - Search by UID, command, or details… - Clear search - View Usage Logs - View KernelSU superuser access logs - Exclude sub-types - Current App - Page: %1$d/%2$d | Total logs: %3$d - Too many logs, showing only the latest %1$d entries - Load More Logs - All logs displayed - - Confirm Uninstallation SukiSU Manager? - Proceeding with the uninstallation will not affect the core functionality of your root access. The root is designed to operate independently of this manager. - - The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d) - - Umount Path Management - Manage kernel unmount paths - A reboot is required for changes to take effect. The system will apply the new configuration on the next boot. - Add Umount Path - Mount Path - Unmount Flags - 0=Normal unmount, 8=MNT_DETACH, -1=Auto - Flags - Default Entry - Confirm Delete - Are you sure you want to delete the path %s? - Path added, will take effect after reboot - Path removed, will take effect after reboot - Operation failed - Confirm Action - Are you sure you want to clear all custom paths? (Default paths will be preserved) - Custom paths cleared - Clear Custom Paths - Apply Configuration - Configuration applied to kernel - MNT_DETACH - Contains %d apps - Disable superuser logging - Disable KernelSU superuser access logging -
+ + + SukiSU Ultra + Home + Not installed + Click to install + Working + Version: %d + Superusers: %d + Modules: %d + Unsupported + No KernelSU driver detected on your kernel, wrong kernel? + Kernel version + Manager version + Fingerprint + SELinux status + Disabled + Enforcing + Permissive + Unknown + Superuser + Failed to enable module: %s + Failed to disable module: %s + No module installed + Module + The following modules will be installed: %1$s + Action first + Enabled first + Confirm + Uninstall + Install + Install + Reboot + Settings + Soft reboot + Reboot to Recovery + Reboot to Bootloader + Reboot to Download + Reboot to EDL + About + Are you sure you want to uninstall module %s? + "Are you sure you want to uninstall module %s? This action will affect all modules, and certain features provided by the metamodule (such as mounting) will no longer work. " + %s uninstalled + Failed to uninstall: %s + Version + Author + Modules are unavailable as OverlayFS is disabled by the kernel! + Refresh + Show system apps + Hide system apps + Send logs + Safe mode + Reboot to take effect + Modules are unavailable due to a conflict with Magisk! + Learn KernelSU + https://kernelsu.org/guide/what-is-kernelsu.html + Learn how to install KernelSU and use modules. + Support Us + KernelSU is, and always will be, free, and open source. However, you can show us that you care by making a donation. + Join our %2$s channel

The images of the files with anime character sticker are copyrighted by %3$s, the Brand Intellectual Property in the images is owned by %4$s. Before using these files, in addition to complying with %5$s, you also need to comply with the authorization of the two authors to use these artistic contents]]>
+ App Profile + Default + Template + Custom + Profile name + Mount namespace + Inherited + Global + Individual + Groups + Capabilities + SELinux context + Umount modules + Failed to update App Profile for %s + The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher! + Umount modules + The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. + Enabling this option will allow KernelSU to restore any modified files by the modules for this app. + Domain + Rules + Update + Downloading module: %s + Start downloading: %s + New version %s is available, click to upgrade! + Launch + Force stop + Restart + Failed to update SELinux rules for %s + Couldn\'t grant Superuser access to %s + Changelog + App Profile template + Manage local and online template of App Profile. + Create template + Edit template + ID + Invalid template ID + Name + Description + Save + Delete + View template + Read only + Template ID already exists! + Import/Export + Import from clipboard + Export to clipboard + Cannot find local template to export! + Imported successfully + Sync online templates + Failed to save template + Clipboard is empty! + Affects the following apps + Fetch changelog failed: %s + Check for updates + Automatically check for updates when opening the app. + Check for module updates + Failed to grant root! + Action + Open + WebView debugging + Can be used to debug WebUI. Please enable only when needed. + Direct install (Recommended) + Select a file + Install to inactive slot (After OTA) + Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? + Next + Select partition + Use local LKM file + Only .ko files are supported + %1$s partition image is recommended + Select KMI + Uninstall + Uninstall temporarily + Uninstall permanently + Restore stock image + Temporarily uninstall KernelSU, restore to original state after next reboot. + Uninstalling KernelSU (root and all modules) completely and permanently. + Restore the stock factory image (if a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". + Flashing + Flash success + Flash failed + Selected LKM: %s + Save logs + Logs saved + Disable su compatibility + Disable the ability of any app to gain root privileges via the ⁠su command (existing root processes won\'t be affected). + Disable kernel umount + Disable kernel-level umount behavior controlled by KernelSU. + Enable enhanced security + Enable stricter security policies. + Default + Temporarily enable + Permanently enable + Processing… + Pull down to refresh + Release to refresh + Refreshing… + Refreshed successfully + Undo + Successfully canceled uninstall of %s + Failed to undo uninstall: %s + Contains %d apps +
diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 396caec3..9d3dcef0 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -34,19 +34,28 @@ val androidCompileNdkVersion by extra(libs.versions.ndk.get()) val androidCmakeVersion by extra("3.22.0+") val androidSourceCompatibility = JavaVersion.VERSION_21 val androidTargetCompatibility = JavaVersion.VERSION_21 -val managerVersionCode by extra(4 * 10000 + getGitCommitCount() - 2815) -val managerVersionName by extra(getGitDescribe()) +val managerVersionCode by extra(getVersionCode()) +val managerVersionName by extra(getVersionName()) fun getGitCommitCount(): Int { - return providers.exec { - commandLine("git", "rev-list", "--count", "HEAD") - }.standardOutput.asText.get().trim().toInt() + val process = Runtime.getRuntime().exec(arrayOf("git", "rev-list", "--count", "HEAD")) + return process.inputStream.bufferedReader().use { it.readText().trim().toInt() } } fun getGitDescribe(): String { - return providers.exec { - commandLine("git", "describe", "--tags", "--always", "--abbrev=0") - }.standardOutput.asText.get().trim() + val process = Runtime.getRuntime().exec(arrayOf("git", "describe", "--tags", "--always", "--abbrev=0")) + return process.inputStream.bufferedReader().use { it.readText().trim() } +} + +fun getVersionCode(): Int { + val commitCount = getGitCommitCount() + val major = 4 + val end = 2815 + return major * 10000 + commitCount - end +} + +fun getVersionName(): String { + return getGitDescribe() } subprojects { @@ -79,4 +88,4 @@ subprojects { } } } -} +} \ No newline at end of file diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index d9e7dd22..74fb84ea 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,9 +1,8 @@ [versions] -accompanist-drawablepainter = "0.37.3" agp = "8.13.1" gson = "2.13.2" kotlin = "2.2.21" -ksp = "2.2.21-2.0.4" +ksp = "2.3.2" compose-bom = "2025.11.00" lifecycle = "2.9.4" navigation = "2.9.6" @@ -11,21 +10,16 @@ activity-compose = "1.11.0" kotlinx-coroutines = "1.10.2" coil-compose = "2.7.0" compose-destination = "2.3.0" -sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" webkit = "1.14.0" -appiconloader-coil = "1.5.0" parcelablelist = "2.0.1" +ndk = "29.0.13599879-beta2" libsu = "6.0.0" apksign = "1.4" cmaker = "1.2" -compose-material = "1.9.4" -compose-material3 = "1.4.0" -compose-ui = "1.9.4" -documentfile = "1.1.0" -mmrl = "2bb00b3c2b" -ndk = "29.0.13599879-beta2" -foundation = "1.9.4" +miuix = "0.6.1" +haze = "1.7.0" +capsule = "2.1.1" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -40,20 +34,16 @@ lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version.ref = "apksign lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" } [libraries] -accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } -androidx-foundation = { module = "androidx.compose.foundation:foundation" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } -androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose-material" } -androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose-ui" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose-ui" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } @@ -72,23 +62,15 @@ io-coil-kt-coil-compose = { group = "io.coil-kt", name = "coil-compose", version kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader-coil" } - compose-destinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "compose-destination" } compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destination" } -sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs" } -sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs" } -sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs" } - markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version.ref = "ndk" } -androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } +miuix = { module = "top.yukonga.miuix.kmp:miuix-android", version.ref = "miuix" } -mmrl-webui = { group = "com.github.MMRLApp.MMRL", name = "webui", version.ref = "mmrl" } -mmrl-platform = { group = "com.github.MMRLApp.MMRL", name = "platform", version.ref = "mmrl" } -mmrl-ui = { group = "com.github.MMRLApp.MMRL", name = "ui", version.ref = "mmrl" } -mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } \ No newline at end of file +haze = { module = "dev.chrisbanes.haze:haze-android", version.ref = "haze" } + +capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb587c669f562ae36f953de2481846..f8e1ee3125fe0768e9a76ee977ac089eb657005e 100644 GIT binary patch delta 37442 zcmX6^V`E)y*G$_Ojn&w;ZQHhOYNtI^y0dB5x*u&jIU21Bhf}7%e`>rSh z1#LY&uAK}92G$A&y5+$IAt4F@2|8jt0LwA}}afFkG#WBw^#$TCX0J^k+I+tvA_2h1Itw`Li1kgyHcDwujll*+Po z$XwsbUNDm$Wt&2Li;oZ)KsqHBet>&OoWy>!E1MRems2?s$vd~r0vYTN=myB^=e|Ep zOPhp4yC7$yh?X{pJgiX}r=I+{B<4TOeoUksBm-D~H<`&WFHZHWL|{HK)gK&-S}>%I ziIqri>?=0OaJ^KsnnmZJiFiG_f?UADlLvkG)=-3Aw~z%^S?4XD<*iL~Me#4^m2$Zj`empiTmN1WnOgbi8v`sM08Y{O`ZL zDN>@uIp~1rj*c9p2a4rsJ73t%3fhNB|EHS9+$2cA79N(jr%Qb1IRH{_$pRXHaK`8dw#}-s*DfXJeoWNogcLPp( zC#5c477mpu8)E~nb*Lev%2hfE^f-7GM%lGk{TaOK^n$z$aidXt)|O&57^|vNL>i(A z#)k1{uik?cea-bP^a_f6K&sH6VAxcnmS1QMy#2JiyY`*r9k9U@c%6$#Db1g4QmPTw zP-deC)iPvGaxE1m;GFQI)sY#;1|iY~Z6?hbnLrJT6FpYiejmw@Q~IHbUGL-xND<2H z)p(|GQFxTZ0Z$`p7!szU_@vn|OEhF%W2}>S`xPL$Q5I20*Q2c#UZ*|-a{ zHzYc6en{$9^L)!5OqfHaa8Tcj#Y=((4}9UN>~bEB#cO1=P7B_!+=f+Q)*C&M+}T{A zC$+v;Z=?Rmh%t98yfbSSdXdE1ik=VA0XEuuTl;a7dqqhIalVtBiDWUu(_dP$jeCeT zg=taDEp4CPa+l{5z;DhDQH6y3?xR^7gTqNT18`DFf(ZXGMz10kuL(@XTIRo_Fi&weHNZ z{f+G(;f9?EH1rb4&On<@5auFNj4=O6plt+z2MsAQ z8k`XP$8pawNz0h;U|{P%{&AcJ)J=l{ibTZ&*eRe2A^E3#m#8U5L%z}D75eMWs?LvQ zEgL^DQ1p!?GLXUCrU=t9~vC@d(;hnKYq zz-NRPA}7GAe!c-G)@#XGGL2KR9#Pk!OP#C^T{%|z?md4$f^_9{Ph=PBs!J8(Tr9Gb zqDXW|?BjJhtGwDEHvYm1Ih5xBA!in0QB8q0A;={r;k1q_HtrI29(e6yr9m|ZS1p`> zw0xGJm>J`~n-4Ss_6bUB*oyI@{1V3ope$jB^p@#E&jhX?_o-ru6SU981 ze)V2H^*$Qu)80DEw0)aq15ULt$3JT0kL+Z3h%PZs68>Feuhj^a;WIF8F0SVWm^;Q? zF~#Eq@?K8P`}4hb-@v;B!!9{S5s__4MAlH1p2A^=x&k&ACPyA4ZKQAeD{#bKkGcWnJ&KdnWOzmd7*3E|K=Jp6|BnUXG#*K^`Fw zoKj0L0wF5~|G`d7z(qj&AMBdI|JPb@h{!++807z8x2}%nPj8A9?d4$A?55*L6-Y{M zXUK{)1o_*6noez=I8?gIK1*hAFLlJ-aK7O)?$HGxNK?lY({-_4^ik$E>|P<9pT;0( zO5uOyGQ~H|b&UV@wrx=JO?Dr$f1X0&3d3KCMt2*{T`gPX5rM7pEOwDm_6jGqN=sf1 z=?Va=;;1lVob8jLkNLM`xq;WsJ~%UHq#`E6{1#{)!e3H)|N2hrwAsh=7E@w{va{Ig z9)TaK$6`yQ+h2{`My4D0MBKs1#ilh=VJ!2}h#DO3B6=G5LMoO9*4jCysFxZB!7mT(pAs+&x3tA+WNE6*L{!uJLW%`O+he16V|(Lf61#p- z+5;bNlZSnyj%Dqaoyi0w?QR*Fk#Ur1kl&mIGa68}#o8sYOeM2va~cKbsjx*jCjs%% z>*9UbKz@piah<FraUf9bX;sb6pnTz6@^OtCl{Xm@FVPZ>AZLr+j` z+FrD-xpS`2$~Op!%w0nxbxz`5@s2qKdnKo~+szzy7VEK&ae-AdEdh0ef6)4EZH*CE z2u>zofc77JU*S)@8*;GBBkoF2Rsi_DGT1@yaEH8)D%3>|3K9fvf%Ld_`nJXrGTW~) zg*MA##_5?aBWzMxmgolOMBDOZS?BoZHsbWQonmqxCYPQUJp+sr&$;kg&PBf`=vTy8 zhfoZeUff7#bEav>zt9W`ClJ^wFQ24^$43&d9V0ii{yJYY)iTAqG0zpjiA*{Oss8tZS-9rG^AyY)Wa|1j~~6Bg#bIlf8RGXZ|*4fzu%?X zvR?7{(l6-$>Jz@e=jB$1Kce7C&Rq9D-~-2+e=Obn^mccoe}h`+X439@L@b#jV*SK{ ze2%A|pEo^H^2SWfxi+fEGW85~80T1ToZC5k$bGTL6zEv(S02%x{e!)y=Y=os5pyNg z&^ajEQY=aW=fD&lf+mfP@J+YWCi-FKJgT7~KUEbzu@^c)8O<)IjHRJX9v6e!R^;vL zKf?7J+_EYE00RR-{f}_t*s_3g1KdSyf5;SgT9D`nLZT&mCJt@8A+*|eR&+*d(?C|F zlOa_+cSJ@bcMBF4I3XKjhqa2%Rhp$G{ep5fyTZf7k(#R9zxmtifV__tfu1F5zo`p) zTSFD$?uo4XoaeiaWtY#p(Ki72Opq8vjb4{;$>ZuyIDRftxo9mN76AYO80`KVIH}Ees*qdW5YMA!`(*T zpVcITlyl$vfU=pG&Ace2Hf8~<9MtD-8TGIplWXoQ%lURZBQC?E55TFaRp!!jL1&huPF?kM)nXq0Y zSkHSGKvPF;8M#%Q^INbzNKOj^Ht0))l}6-h>;<+iJlujO!g4Oz8*9Ax2(HWy6_~($IN;e!=A3uP`^0TeU)dC{b&0=;1 z!JJ~urGMEwU4i57C5x--mNIl=z?o-bQNRYsKx@pW!%qQFPq%t{*vq zeJJvTX06{ea*@9AHC~`5JTcw=L3LZdL`~Hlk_@!wq$s}D=T9%4-iw}?4+w%M7?$;8Cyx``?!aOr=YFU+7<~{;UDH+XL_5(v4_nLN< z)_I+|aujQA+|t^af$2z1k<)Ltr0y;k7|_&Z^eN4y`Qz2UjId8mSsi2`5(?}9P$sGg z0r5}bTxpT2QLyehtu}`>8JBKi=f~g4i%>vE9IOD4TG4@QAu7h4P3dIDYh|B$EgOze zsSS6WwN9^IZ$r%UnN^T;LU};2)XyTeVQTuivote^uqe5hMpA_79W8;9TZqWtVJreT z4BO^Bm7_|xyfhOSm@GiLzrR%pa*2L*kv0O54UUx7Fn*rXRt{*LZw16Vc12XMO9F~D)uL+OW8>IOoiV`6Y;;N{kQCzW0Cs!IhS9sdtxUG`*{jq%{>mF?p z0@OH+uj=)nafI->pI-+}=<5n7Iu>x4v_Y1hIY)5@QW-_H~dmPzJ z)H_p4ln&SI4x@~n+X5Spsr|&rrb#u@3G$D`4LTW#DQ<5Laiy(Bl7Fc2K9L*0|*znNkRynffSJJ zaLio&qPSBb&5zn4KmfCqs{npIO%Xag3T=$j&CzO8;)?whCQ^5QEQ49=g3MNz191T} zxw{4Ax`3ND0+{}Uvi5V~e4yX4;*KWadQGL&>-%b*p?#VrzQfPh7M1De)wWPXkDOE< z<>dw*-_FE{xeksr)(k&G@#03M00EE{1Xe8(!$K@`3Kw3SG{k# z8vPRB(Wuj^NplhePT4~dxYa~+XrGAaduyYUo%dK-flcB0nwH<{aEM^+R>4ER=X_>A z$Y?8i)rKB)Zd^VW`2MtTX6N=Kx3i^b$p2g2tKsdZRY)zQ!B^y|{Xu%vs16u?7FAb~ zBB9CzBU3N4a>TyeCmN#A_Umev6)#nQtavE@X26q2 z_9Oe|_|Q9W>`VMGOr3cnE|TmaPnJp|CK3Z3IMxm)^P6(eCN$3R$pmm=BwS59Iz}uv zh_mtStIzK%Ftx!DCZT z!^$DGi3F5DL<#6Oe5vjJD!7%madOcZX=*A+$Ezwx&snd>!(Yo`)vci$xrURt5u6#O zM})7Znb^#`hFp^l;|VOYbF>5#6*hzoihwWZK-(oJj)DuE8LW8K#K_jM&fmXnd@$DvXp$|uvQ5SWGu3evwGe;}6=DxC~ z%0WCxF#rGyE#~l!R37!(AUsGbsD-yE2a@_oab%Bnib>Y%j^Q;M`Htj!&Tq}e{dzRr zqSqTzA)i=iD}MX4FXRtLUG&tt_(3vVB`18$0%99@E(Lo}=a+Sx!V2#WMyCwc7(a~> z#X&b(F(wB^mc5IxiQzw!@pJn_HS<+QX|WqPet>4O_tcI<=n2Jp^tSeBcUHKD%sf8~ zF70vG3=vopQ<@OR-w~%JMfbl2Y+QN+$OL>4+v#))Rc~ZIV-=^aXz{u-*Ze9;e z`972;UHl970lJr;^8FfV`1@8xK@vbJj?g~%AeT){G5Icv-VJVLv!Q*1S|S9eor z63+tjSKkF$#(#l8`}_Oz1Iec}U)hOf$U9fgcF|3JOtKQtR@{*JVZv=_Q8y1G1CrV_ z;UbSJBv>Mzg_?FXxw$0vI>I0n8v@PELOierQ z%bQydx@H{$rh*{`g$aivh@OJD1Q*hL(-pN2F*Fp8C7a|3%4JKur z8~}a&?K8ZzZEuVXNrdzBa$LPi0+=E&vzKF5XCUd-U_0sm}a_&jn+{ML;Z+ymwj*kl7 zNz~0NC;lg(m@`Nf`j@x^Ygb}mkP}c`#4`%RqkJq@^Ibc|EFPTvmNy!v!*Ir$ou^(z zi3aM3T9O5RGkx?76|+|sYd#MZmVQpn!tzmWH8C608FWl1n)cQ#E$ z^o{Y(q2!V5@-H%J$V7ju?jC$qAY22*^vo;5C#skwW*b9#oMdHemvlkrF)m`)3`0L} z6~e>gHYGkvxv)NoQZ6rN7LV956%gfE(l)!`k!PrBimCAtb-vYzS6INMOhL9RG9t(&o~J9n!|)&CvH1@Mm;M1>8sdLD3k8J)Ku!UN2?-CxAUTgjX{XTn zo0AlMTzG^CLbW_bwNW`q75uPgay8plwqq;#GA6#_PUHnMi~6w#@g%N-TRwOWrvlMG z=h-~R;%NCg7ZGeGa2fu}-hrWQy8{OCxQzIE0j_i_5VJ&zoV1x(%|B6GvO_##gD`@{ z{@oF{yKxW!xRo!}{l$CpXGOvDD5_YZbl$#sTdDere@VfGDuR*aia;ft8Ut1d4jr{! z(C&m`uRV++Oi!RM^w|#;EBK6Z(k(nUL=^H;by(=P7bz2TF|@Z;q9gj_=qb%I-4Mwc$Q2Wa$B|9 z=b$yl$f69Gt8A*=bYds@q+dUP#7&f*S zW1yL4(`L;Cy&uyP;N+hO4oV@J30ZaU6;3z0+*f(8CQ z>^*JS?keN4b1I)v7y>{ABuyn3@jre(q%?~nIvhj+$JeSx=Qr16kQY zE6)A{LPNEPi6=@XO(Ow2pI3zYK^z+%uK2FuCrVq+&|V;r)Cjn!E>!B{Vc+8e)#Xmyg~^hI_pn#+Ufw|~`h3XRP&l3)ZoX%&RTB}Iw( zip3J`t!lNMH_x2d5YQ^!?ibq{!*WnzLFo&*hgKo*la)~f+EPtEVH`88kfMlj ztlEh!kI-^CPhjku@SUpV;Sjgz7*5g%Drwy?D#wYDV>0~Sz$)O%Io)vcRKkAZt$GA& z(!6kXr9gY$dwUyCQYxZocyk-AATmUlRah(uuRy8{^H3)fz^-E-E^_P+ixsRafhZdg zCQKaM#-4zQjW7N1ovOnRZ&)|fjyT&Hl=u2*&P}CI2MvO!PW2t(8&x~Veo4hD5~F0K zd`FiqYKM1TTm!t)%J#a#L@F9HYVlQMrxH4RNKe%GktRtg{B=ixSFgBK(5Gk&+b-8* zq_lagx-ypCOv}C`sJVxHcP__+WYNa6dEByg*huh)1W2ei{8e_&6V4dm=fes1%H+f! z;LiYTq)L)Z&X-+ifSFTBoWqL2nPRWL50OmpYIYhQu5u=H$h#YprdCZDx(yKKmbow@ zt_CBIu8C=mRip@TC)KBYM$6mBF2Rg+{?7;NNZZ>i|5pX3{#ONt=!^lHQnZHuECfs2 zo^WGA@+0@>KeQJTE*??8ND%i}UPM4a5dywo1Y?}-XjZjS{khq#EYDe7EcMUNxw+k7 zw1zpmI|x%3)6mv@aYKMLe2JaTog27Myb}0(jb(IjiRd!-w#;i^Y3vB^GWGK7ROVu z?&0ibE;~*)!LN_WCG14BoBm0_6u$8ly$M54&fh&6KfyQdK#etU2tPMMJz|(gKow1? z=ty-pg!Ygxa7T^>Nr%jT*c;jKtNKRRS|D}1r1l!Q7=C#m%#kc(*q0tKD`f5)SG|CT z3=1)zHl{du?gV)Gy|({TrPVP%-QGl2q&TGveN;gP3f_dG7$^Voghu%;PK^gT6@6mQ zUBG_#XTM|^oYg@AV1P9uPw(B^P62Dz*{}4FGfF@ZP zD~4I7G)!PTYy&L?rNa~=m+9ny>2=t@E1Wejmzy- zi?Mn|#uVNpt1>!31>LkcguaE&vTousNf~3TL$7k>wwJ4Vy?{rbu9&(bV~#iW^K)x! zt3IR=lKp9V(KQ?1J-t+ZUKNXM5~*)48bu2;#B5&6l;Gs_99y#7nCO!cd(g^yAwajs9RF$_-|skMcGTm*bild?wpt#1@WH=LTLF)B4WUW79mE}> z!}{ZA24(;aQ6j=FSQ0a6Lf}w1vjHaX`7W+*xjQpUa`oI?G(YD!qDVvEQvbx{vpF5_EqhcTGPfyJ@@8$}7Mov@^N4un=9?j=llt}$ zzMcwylO`K#(!FcMKHE^)gI>Ee!YZ9X7J`ncX2}MS#xc4v+CxeoDkap;kkhhIo# zA>4oiWsYGL{l+Kjv6CsSYDmvEeTt%ZQbQkdq@!B!ZHe%Uv%YwwyYEL#?%5@ZEv>54tD#iB`QII+H>YWoMg!R>C!+2TBTtsGhQ z!^%SvbO+$ZbPMl6B=Wz&w9Q-wYenN)6Ruk?XWj?GQD}GfYm8Q-b_VyRT1G%<+|07* z#z7Qz(qMT4AL1n2hFoYyc*XWPzCHO~PA`vO1a}Kl$3hzS3cGT?!je>Cz1ac6_?pA~ zjK}%aXk$G(`!3Zs{9w9*<}{b5XRvejJQKy$fZMaVB*V&^G>-Os25*&bKF)pw7$;sZ zbK91Nwg`S(p^``bdBtW3bSo73LVcvQKy&bp2y!LoxsI5SP>vzukN$xtI;s1CdB&YO zs85Lh#wG@8ddf3!Ft9mjFi;_zB0xt2M-A=sCyXrCce}Z{IdxJwnLyoe-2unS@5)wE zdF+j87z`rLD3IhVPUd~qspIsiUwxl}T}?Jb4d&&3cfE%MH?z6B!9#xJFmkdf8DU&L zPZ#6O)1OaM3;@3`k^n0-tQ=l{Uy4sNM~Tr37e1UUICJM-Nk$X2$cmDc3P8RDoOlXo z>)@9LtvrDTI!(svVY*+XYHg4Umsq_gspf0&2iu@MDS#tO)|Sf8q_mQg*Oj9w{OJ{xHY|B;F{RkX_kZK}Z+Huo zSff$(#+3}?OY-DBUJ--Y1B~U%$qKJZt;qGhi35oJ01JhQzb_)|swoq`<$_y9Ck{6{ z%yNfH!i^yo%Rv>DPi{b$LObHPO%unM*vi`{m9n2kP1_REE8@WGnrnj1jfNgb;{|a?Jviy}~N=2J5vH zFTCqWVAt@@M}g^zuhjGwvud`{p9E4F9tP^k z-Cn%iLTPpDbfcmEhLCj2#-lMd4Kq{RkzobK!6D)%DqVD_e&KbW7eI7jQ%kt@zzjRB)p*2oKD7J=BREc*SoSoH z_6NQ?fw^bQ3}B%&(7K{$q?Qe327xF3kn4Lq-ZLS^$R=cGy`{UEV;O5CO+zKNw+j0Q z-b_X-jpsK4H<5xbI1Z@>d&#?q(7mj!HL3Hyx;(z9`G<|t=Hr7!spI8cMea_&Xcn`F zQq*Bi%+H@c%yiN2$P%G&4)U4l;kYF47D=MX`xuy}ZUA%`QfdEu-BV`sxlxl2mk=-J zm2hh912wx~g_XCg@Y77_`~HE-?L4=y9anG-H>4G6dsLR}O(^!JE>5Ns5hhKGR~ zo!T16FC6M&F$-qZA#3jsERGF01}6{^cDYeT3tD-pEN>w0T@8VeOH|||fHI`kQ&nua zZ+@&MA3);!Z~;D!9ykb<8lrX-P;NNm1*$#i_zL{?SKdXbpG6_{w1h<*<|ot3cb)UF z7HNsSfAJ&IcK?8+tu4n;63C5l<~W0N@(%K-RUKyjg`lcbY4npWa8<>hL0cC0)1)`NlM1EV zj+bX*Nzn+rq1m27^S$Tp@)dltilf0G(5kJ^1v2@6p{@eP=SM}*1&;~9#${d{jSnYN zlIf^8#K?Ua1{?Su_ZRWOCJYf6nEx>q zXY5S|go#sSNzpjo^0{hlx7hCRdqoU@&^5ubwcD%~a9Z-bQ7p2v7=_`U`i*KT#SqyV z`==qr)J~`ct!_tJv34AwMt2gqlYd4rrge6s8KG5*xrDM+DF!jz*SE2;L3}v&_wBE| zKrD=+o<5IME-^x(Dl~R6QU19w^_iIGX6Ex*W0R&wPKF_TvieeLU<=k@P=3nj3?iAs za9^A(F-wx$n&@VlL1QhtS}!~xr~ zmvF~$Y38~WU3Qdq&MWTU@Jl@NygoxwYqrxB659R=dYmAP8aU(z3_I26@6(Plf8jqk$=f1 zlpk!s&HNb-vn5nz2lZW(-`^1hI;OAADW5X6dQ6NAW6fV12MyT-V(2tRLraGk;|L;@ zEGBJ5Mj%&h-(>kGwsvJJ%1oGYZ6Z`1M6Gz%70K1HvMv_@<4&}~_y+Mt0?6h1ez@WK z_I5ukush17*vurxm_n^HW8eB$X?#z!~>!3#?01=G_b z!q@Uc_oWZiU#(ZU5%y%#Bz{kT-?uvGn71+z$Bq&C8+q#kwWYDA95HX=AcNOU(GmBsVS~qjwA@$%cMxuayqGV*WIx%}Git%fH}J=;B>d9(%Rkh` zW4y*_!!Bhb#cg*0?l=GNAwa4Zx_ZtB?QpaZ6<&^1I}XI|Ot|V9(vaeaV-^CTPJJiy z_3ghosr4R#0OemGrseP+;)_8t(IAs|FGk)$IfXx0WA@v+5 zNbEOG+^r|;FFy0C%=fC?e~G;vOlJPQB-;wQgn4!|SFtG?Hk{yMxgtIjm zOO}l`J*+$>D`)O$>-mccuycD=&1-B1%nn1g@qwSfz%zTX6{~=Jy!EW8CzE@ww#J7< zkEKLL%JV@Va2Q`uP%R`gN^Wbz>}{DVeS(I^+ep0h=#MEsE}$C}ywtw$dX08Fkw-N* z!a$zfus&)9mxb+n=GK4P4!WMYfn#_0C4}i0()>`U<8AZuWA}!J<_gwV{B08<*bXDK zWw=HUBSiCU@*C1O6ZobK^(uk?9&PfOh$!oyqD1~bMae@*7ND&6FR6;gM-n>thY)t~ z8gVJLKu_Iyp9iV1vp+j)|IxXevP(3G=g<$=u0IF|@~>!UppN+PpCcMVuu8bym!g^u}vves#PwQPOv?)_<7{9{m@eQ8^=||smzq1 zChZ?VJB{8D-=N5K) ziK%|Y#IHsCgP3dV&h&w-bgG}Q-=mIBJz z^gHVwb+#J3wqqJ$&_D=J?gkv-8v-U!Vu9%v)ap;Ig5~dsA6;;m_hJ)9LOaHyO`|TC z^|Oa}Yum1vQ7u`Znk==ys~N#&^fxkFvQ(;i=vR^^dcS2^H6X6Owr5)M&&84)MGV)- zZG(^1`K>G#2I)4Yq&&c3GkrV1-^-0NSn8BGnCYM*`uzHN7w2b`lG?);tDhG@wbiLI z(Wt$SsNiT7Qp}*qEmr=R$K2P5x`DfU<#g!zTkr7e`+x^V{e%$#SB6DPK>7b#+4-E3 z&)UBiAO82^IsiV0f3ajg8U86Ok-ZSo8GJPNgL|r>P$?t`NmU_9%>qGI(Bn(T)aI6z zhg~G)E73dTuV3+$FW=rN2VTB^C(Xc{%zWPJ*!=k1Kj#1w+0b}#bbIu6Mb1s&x8RA0 zGvSNzsiTh?J3y1Q0u^M{?!>x~PcvsFC_F?zwi=-E^8<2}uUauRw3HQ-)7DHlap583 zLeIs!ALqq#;C3vMaHYoCSyEQ9Ghy0HkIhnd3O@@!DbsJ5yKDuiSceb$to)6f)lX9B zWmqIYS@X}a6_I*>f71pd&UKKQ3w^r6e7h);}z+X1Rseme(2-mdI-4 zq(mk+TLOq#(b#jbZ2D9}8|9zn;N&)}iG^h#i`X=Tc7=`~96m-*bMf|T+a{%+tNMD~ zMo3FTlS`eYzHamY^3{Qt0AoEF;tNVq2#9>|(9WqZS)G=sPIC9Un%Ym@Ei^k@NzTyB zyHt)MX>G1+spXW4B70o_-b)2Rgw5Win%jTn8?#8fGyBN`*Y`I}1<>>c^*>6^UDVBh z0>qpFat{BgZIC|ox9%WLQlKEXeN)I9c`Q*;@4$?(NCK{`${^*4G8y+OUJZxVb=%u6 zq1xdb4wSs^4%l!`LIzT!^14dG3&r~n`G#v+#W`4{_PLI0-usCTg6WBQ%`aCjKkTQSybb^_`mN_WK{tXe|3c&#JiN4-ct-X>Ck|GO z^$mo<=-K*ly)7D^QSAGB7Z(?c2pm;|Ylq=#8Nrp^Llsu^oJ&t(@K_7fHB2GNI_R3I zoSdo@x>*i?T=0LFB>uQ*dw=#}ff)oV=sY_qG}JoZt{+7eTMrz(s;8?=QbjX|?H>T% zpK?3vfzbUxRPjF4z(`gR_uyx*!NVLqEv;=N^CIH_qn<}Q9dLM1Of$G8X%rh!C`#M# zJ{*PXi+wN5Tv!6{0;1sh#p#7XLNP&qpyw=UnrqmlU>Z9XV+*9{6hov&S$&Ent{xnz zgcm$wnrA2*28=68ozFLs^oGKAUIJ8FPKHZ3wJhZGLq`cw(K28zL((_I`@;7-thpvj z;Gs)Us-|uyEqHfoE+-5R|B5x1-is~W2c_Pdd|5gDoAE0+r}Ca!aiWsh$&|`4alVch zT+tL4Zm6W;!?v{QBeBn7MR}#)OpT_wRApz-wuk#z#OxdbqF^#>+Jk~73IGN9(IB*< z+6VllEYoarEE%j=gYp_*w|O+aS3*_aA;*`=rNxz~tm>6|f1r`EgZ(mhV0UuXhjLZ@ zuL@1lDhntl`C%g5B}u|Lo=#DRP^0h%CXSlBEwZE-r3j#@^*!aaO7VMGXUB+R-zuw^2bY)3xT>Ar>2-6<#w{X(xe2*QXRws{a_zG z3a=BJP%qUFa6fs4Xe!OB%GY1lKRL-cu8Zt1`c?*A)mL3j$C!)nsT-vajWNDVluwKhwb?1v~o?)z-U;qYd< zqq7xcYL|=^oP+sGIq&4Z|FcoGsaz6d{>>;T|8IbcN{Ik;{%d!i!0$RKVzAWE`ZMTm zI+m5;+KR|!9)w8rLqkw3wqfo@?K&3CgiLoLzBjWLY}y5+LlkoV+*2+35<1ekqIQ;J z-{Y+7tXFfu$LC*!9zt^LEnC}(68+Pt4P9h;b%LcyvQ7hzP2t%;tq!g79XsX_tHret z&){758-S=xFQaPD;-FGQhJW`IAKpxu3^&FbYg0^|oQ#Z&qMGSzQ3lkj?ART=aR&KS zj?O70Aq`o$S{k7bWf-d}5tR|Dyfo^M%SMclZc}tpDjUtVz44A_^ywg8o2Y|~gTaFM z(e|qhlXl*99+J(}a}zQF2Hb7t)@x~qM|iw(UqCVi-Y$iWZwx%L!{)tMATbrl0VXcr z%^Anwj*f?GFp2~FmxEU9VH{%v4qMj=Dli_|uRtiYL|7;a$e58a6QB;P&bqN^Ij(AD z)=|n>Gv#y;rTDt##u7~?NitIJpoEEop02^P`Yvu}s3y{>Hj?=(c2BeClvCu|h#1Y4 z8NfO*Aulk=sLN}cK<~=23ofsoP|Tv2MrJ2QJ(?JLQ|d%cxY(bk=8cwRV)+x2TyL9A zFLQTAzjiWP*LeytNf>6TP$M)J4nDha>o(EM0wVi@3>{MYree+5i4(B=hNV*04u-sTMf32#Ox*zVWIp6Sf(ZRTL6T(|0GcqK zx@zd34lJN&Zait0;?Qr}x%q*viRn~!y{twy^K_;}u8Ar6gMXP_SiCMLeENMH{sgG% zDk|wB2?>fhRZ?13x(Y|oQa41xQI}CtQ(wIWMM6XzZ*hR+6YHMl{a=r|T(0RoeVp#QR>C|4pV7Z`XWv;jxAq*0g&Z2IX|V1 zi(B#LpT2zYwi%ev_SO!JGwP-H(~PrlV?pgUHN;eCg|)`Eu2(1Tw@bXPD9DqMZ%T07 zYGP*h$fECeYNZ07sv%e0a$5D~FT?y}pMI51M6b$i@{nDKjpB-M#hA51W=Gp1&c0Q3 zY&Gi(Y9~qQY@Z%+s@zdl1#n}@%WIK)4a!X@&n#%>@1BUR-@!haZ(`piEl(O#Lnp{g zvB>s!2IQAGy)F^T_Z5I)7&G-tExOWhh6){)*u;@>VrwpIwqn<<1#zO6LLd#D!c)j* zD%U2NBBZXj*(AtdDu6C2Z>0i(snh-Aiql9Ld^LvH!NMcwyM@&afWLQyQv+J}GXdg| zxm2qZ$!;2i0jv@;q)Tipv{^@QW;g>8Xagez*jMg%b%g5SjZRr|?Ct7(=r392%lnC2 zYMOWfK`xeb5e|#>5cmT$ybyHrJtq;AmSpqy!~=9O^$6LM<=z?hj$PsCgZ5_oOgFj^ z2zcYous)bj`NmhefR{S@a6)+`gLCY)PsB2sm66&9%_C3Bb=)3uS8(JW-Z~o#qta+ZL({y>ti(OS zf;^L(wV#n3tpeA<^OJ^UStdx1#C4_*P6&nIy?y&sx}4avV#DTdyCb~1I0lZ|%b?Z& zk}{dM{XGHBqD{kb(2Ou=d8_uOd>11era6+6E{Ne)+iy71*4A&vt4_d2_X3X8#VvA} z>X7^uOi6|R1Nc!Z2x0dbe|}`*h5XO@P044WE|pbb#SE-GGD6c( z8ASh^J%&(i6pjaX$?}czY}%%bo^7gZbBjwKr>D^qPpye+cnZnyhn~q#4;d~|i$fr{2ck3c{~y}K1{5X=^j6_#YOvcazJYUMj|)!8jtC3SIOyb9PxLj z@A{igCn(aDo;-T|Ed4FjhNknxliS!mF?$(h=)&YeyxL?(^-*S0IepHkTXd=$vaee! z{uiOw$Z!tm0>_@^q_)?LP*sFm=f!hxSntd&)K`R=FUb&RVptez{m`431OwUM8g@nC zm+f@givgvD7NDi;nUb}mt}cB!Z|%%0>&XHh&3U;=M!cMYxuW8d4w{=>dLnTWB0G?4 zSxQNUCC(3sI`Wdov1+MsE=W9C8d1(j>&eNl9+52p?E9(~^e z6yJHmyd8UKawp1h_Uc3nk(1*SA5@iUCUo|0`?s=Xs$D*nhWOYUaC=qo$TG3}uPqmhDit zieQL4M$(LboP*N~^g%lx5H(XCF5K)!4+Yd~P1AGg<=aQWTF!g~-*s%-sD}RV7pyYMNovcnSOQ>|*vFN>Agd8o0CXq?TK7}d#+ZQhvFE?g zIKP3pDgMH=IyK;>|c8K1>S{u}9ry>D} zHS}FhF>Rc479J{bOs|k>)hYL4;!BpbN2bu8P(#rzBMXA{3c1) zFY|yC?0`Zvp;W7w-;~Q9OBqgMnMtbS`+p>zV{oQjvxVb{?M!Ujwr$(C^CX$rwrx8T z+qP}nXWsApxT~wW_OD%aSM6Ti>uS7Iig(Z)a`dN{wlb4lXeJHJHkt&$X1(!m>rD8( zt~Vey83W~pM$6wA;_!LaW5AE7j%R*h0Oxw}@Px-H5vW!jj)3o?S%pEb><(z^Y76$45CfLyHnTAG=56F)j`<`-wQ| zO4w5{-JNFm81s!NJlPd|lr>khu4zq!vpqL>ic_E>4JV!Y?a z`Iac9B#Y3+Axn<~K)lKE?PFL(c@H8z+8Y2NPq(>&fG_Y;|n!3l{w(-O2lR}|>4OcGZC54ViSB@{m(mC_6yXuweO zIAG(V1^`ufo6C!{=9j%8Uncx=(*%#|H}?P32S!^6u2m=?pkw6! zH&vh}zH*}_s(4`}&XhtVI;ar=6cKq-LALzzG!zv+bCf|)WEiJ@!d1* zA3-gnDe9i!wy;kxI?&$ZFUAJpKnDH!U8^j#XN8SO$0$z;O7xBig zOiY_qVugyeEBH<|R^R&wN;Ad;cv1&^ov0jk0!5p>hAuKx$9hrh)(BRz110s}YJU(Z z+y+!Mn-084>aeLsT#}l2ne?f#M2jZ3Ca;Eyo`rJP4LPI=c}l>K5sox6a$tWu|Ln$9 zk;G~S6kjb*tJQuL^lSju*6mu~yX-0#{`oYQk;|F@%pGGQYsA2|3-_D7vUo6u71s3N z=s$8^2^~4IRIv@cmu4y2!<ZRwjN#LUhIGy)wO&Be{lZk`&{%XV&(*3~WOWOFL@IO}{B z)mzrSTd6DN)mxV(ML*8hG6|Ao5`b9!$8GQF7vGbO)PEZB?WnOxlT>@o*)@*+X8W@% z+LhQLAT0g#-@8eKi@JiENbN5@$FW8$`%X#$e7GK(1#fVLq)w^>)Rkl1V)Pa2z-*Q~*<@pID} z(2O}3Bqdi;zJenjy`C0-TCuh5*M%ind1&S|TtCB7=mfAZ>7k`f)97(QSV)Rr3Q3EX zDlcMAc1=ISm86CvGN_+xuvAf@Yql(!lpw>BeOZnP*1RD^kex42B4eKF;i!^QPAA&~ z@SVXf%=qDp^7(Q;vpIg|U#KCX#0`EHrnzhX`1pzqw>c zHm36wx~pVN97<_-y0gU!TSKh-Xyq;N++!55R;u)=1Tt^z5<7-5^2Ty$ZQyB%M7nB8 zqz+JJGt`JBgR3>}sb~!!ve}RJj--tPsD+KEI{eP~5LA@~N~tOWs%_*7)T;9|I|n4( z2f`-Oe;Yu4D{E4S=#|Xf8rYKR<}W~N4ISe4)XrPgCAHYZ{$7R=7=*4{neR~25*Bn& z9-LC?pt^x`YF|poS+;PqYhVhGDKVLss%T=YUHq+?Tw|ydOdJS`u=KAU0U}lhh@#9r z4l{OOxmhBx&XHxGTQu4x;)>176!9+7KrYUyWm;4svGbxftY`^53QRqTImL@@?D4Yl zO2Wh^$7E1O>w$JBn8{BfBBjd+7F$PVX$K`5;mwM52Cca<)3&n^DP?s9j#*z>)DIy+ z-_P=Ck-I%JzlocMOVC7kgpEW4M8qNAARsAG-gzNo%bLz`=g+vfV2!cr(_C%b_a*wd zQdYN#3&80{9BSPnl7LD;^>wjb`%l^K1R$C~w$y%4Z@^V+^16jpQ)cK1p*Wl7LPYDx zi-WP(M~)i^R|X~Wh&527ghIPLJ@;aj1`i+7$6e;*NR1Ydr1c0AV@|pPgvqQ*Sh*sS z#iC;d2Ftfq)}wv}<+FOxE|^G?CB_U}3D2P_om>#AWTroF_8 zYgw#2tUd#VJ!)6C5N~*`2APiMv7_w

WNp5~7YD4``2J+2^JLXSDAn1#GQa=Wm~d2R({ zo63@NZ(FrJihQ9_^uk{wU5VSIS%+H(kcQgDKY>6>gbwAJ$%XB-?=Ouo$MLDV2g7VW5&SW870b@!rgWBdQKt`zM07NLOKQp@?CQMZO|HcU$9EA* z->CPr3Vl_okGT}K`MjU<&vtNB)pwSA{El?B2M;YWFt88v(*UK8Ts#p?snMhU`{W09 z1m74uWQW|)p5bV~wi^mL>B5FHo*oORQ^rwD$jctPfj+8eybIwzYXmcE#Fkx zD6{ee4_e_MmM^+SP#&s^wt~j)S#dVYWjdw zA-bFr`}0RsUzkOzS~=ROGiTu`rs(c}s1X$0wUkA)XWdqvw&wtlejT0s%>Dpyod_a(owhK3$j-yt`FYiO8p`$LWb% z_y@R>D?Bm4L4n|tYDY)P@BBWko$iR8>q{+ZdOj>!qA*g{mf4&4k$WyWyXeO_gHtW( zU4~_u@ciaR(FYL-Fnb0*m(eErkRuQPhl&OOVTyZ{e>4}|rys?)oY;Pxl`e?)4ETmH z&KDQZnyg$A{>l3lWcE$j4A1&JILYD_nri#d4I&3H!jzfcV#4W-69y`x8vd@m`2!!WORtD*v)GwkwKyjP|Z5`>7Y9On%F3{OYDcg91!JG!(G zIaUI=|J~LL5q%;XM=OeY2a@jti&?}N0xd3YW;2j3J%Yjp2F{s@!T9@t#Q4zRkBTm= zZ;HDMI9l@W^h9dU@@Z$R!jZ$AqV&KM*q1-Xkv$Y`cs0iYP^gi>A`_v3p{bD^ZQ1H! zpK(?ZFGb$GKH#6RzENZS=@H$_j|^r|^>tR3WC2RyShvH6OwLZRkW4`AEdkiN3-Q#P1 z=*yj7_J9x2ltKK1keYfb$1azm%kvuG6LX9_nfYn-TCU31vPRLRnUQcSjZ^ODv>5LUx$J`!l4J^@`&gh zLBMK+7^x{1I?g+kU5}}nJSQkO><^ff!dpX&&PPO7k`gTqy*^8U0nQ zpdycJ)|i@z5sv6Ni)L`w392E$COm{Gu{JZzH|`l z&_+G<=yYky9O+!d{FO-FEqS7id9c_Q$>f7%jJid$OX@za?b02-6MGi!w5+VD&Tr>jA-dJQ2MJ~r;?%;fU3~qz?d_LtLT659>X;n1NTrNe`*q1e z8l8Zc>P$^0r#xiopNnu8>iSJ!4DEEa-Aq}hX3Y5NZ{usP<*^fG#PR}I@qRt{gvtj) zRdz2a1?9v?rFvP3)Ow=qmY@A}>7Fj=ys4>!;FOfma1y%|@6fl97ycYbf>V5m| zMwLm5wR-Wu#ot>h`r;z@*HjcHSck#2>Fqok)oOWwD7W+#Y1L?C`bs+!+s*Pbm@)1? zCj!5ba0cYQc@7WfBk~!ro_fiOb4~rcu8stCd?!JZ7=f3}-t8I>sj_NBk|!GU6f)P1 z5$rz}gw$w%Y@~Jwxv09qLdf;_1IDijQu>L#uo(_&HwT@}XiGwmEfbuhMnmGX7;%j@ z71Dy%o&VtJH~tLm8T{v6k-`7?+VwT_lQvt$4??UuTr|ew6ITPk>{kS8{5Q16{KR(> zzgW@bP0|Hf4f$#W;mQ>jw^5wpdXi**G_i0!4BuGHs*b51ehphBfsAK*hxlgw6W7z~ zTaNk+!|DYF^p$nEjS*aDrMa|9gUoh4<*WR!%3Y{700b$%x<+RxB!1zAfBR z#xAW{SQWJ>L1mJ~Tf}V@+F_`?&1ERR?ZMX`;12Iqv zdWcbf7)&*wNVIs3|M*^)Qr)a~Cn^1HkNHJABq{@O9OJR{b&$87|7AmLv~Uuw2SB4T zNWw%meUAwfryJVYrpUdYW2fV=uDyH2R4kUryI#z48=`m7|?b?2XrH$kwRG zV5N%`;e2&$y~B80j!PL58XPMv?yH}zIZH}w7VA(x)@cn0kON{Z7#FtC!8%GH1HKL* zqsY=(xQDVWs%3$jRwxCtrIMe(+0la)DjI+#nKC%6a0NlM)J@{DUai^LP;~;+Fn90~*mXvc%=gWPWbUN4V|Pya2t-?`l`>uQ?GvPnWYtdd(~?r9#TI z)XmX)ic@0`+Jao7zsDyerjhee$AW7SMsXLRLtR}ZL4)*BqI9KVTQwBbryxmJGR=N% zn6KBj_{;^CKx3EuI+b4`lM3HWq>+M%i;Mu2zlkpmW$$O)8>g>xX35_}V z@1Q;_W6%VS6fOn{vV*yxvsg&}OF`4b`;&xpKxO=+J6UxbzmaR&6(W~?|G--d(+YsH z>4|K9_)hV@C2}J-@^+ihBea%l$i8C~bTlcR7?Z#a3Ug80QFS3Byfss@JrMPVNHs-i z5X15{HkqzKGW1SOswml8S2H7__?~8JY~FNFAtl8l)vWwE>D^`tNcW9_&`-$saEn{Q zbuXoYhV^+N<$SiG;`4>=bLD(%ZwruK%=?8pJrDdr&myrBjPTpyihRH`L}sN>1r472 zudc0}eG{#;qx}+=)R8)~1}~Y~%dxsw{XJJb@^d$^|2BJ0K0Dm(2Ex z8Ly_7KL6I1JU(U-Lhzp&|Cu%U7e1L>6X*`#>(H}CnbOy+8PaWeW?uMtK~{h?XzA+# z-_l1IVqyv0Jcd8D!pt*V_Q*R%<72Y&A!r8l5zm=2g6Wx0N)A-Vd&u?3xBH=A^lEMg zp)|h+Ezkt;dlj74wG&>7y}a2OImj7ioF7^IC&(Bl$hqm(;f&R8^BhaTc)a7;I zyx_ZSTkiLZ4UmODtX154sVU&>O_@b^7Wu|=y9C0i=?^JLg@{ns*a>zKMt^sL@HWQB zS*+%}b;>v0^R)0O&DRBL@)3CRTH7rvJ|7*JZq1Tb`>}Fk)u#7e)QoBkCjyM*1S_SL zLZV4CNCO))Mj+6gr6A%d4i^k}dhnzkVyOt(A@wf>dWMEe{{>o-=i;O(=VVK6hCiXkC%zTs)@~%nNLfA{5 z9OhQ-U!J+lzY)dNJUfGORy$uC0zI^P^esFW+G1ON!$IQ0s1$vkILM3{kuPfwQn5 zbkf~3@HS`PHER1l2pUpGK;3H<)q(EUUw3~5A&B~2XSAx@d9lO&$HWxhTM%t zn;fSsoWmN2Hm|sokdOAx+}JF$O4B%dAdkFv7W_Y?5U;MG?{C8Z!EVaUz)_q-ZGm~! zr5SRNFCXgNf$GvX+(16oz&$2E^OPMFz3Oe^-`))~x^@>jt2I&b$1g6UMbC32xf_Z# zQWIeP1kAlR6Zcz!)loNCyWRtR$oP8vDM#{VZ{LD`Kz@fh)uwOxPH;Tg=^YdkU-Mlm zKJdNmgJ+K`!Y!Eqa8*P8D;9)6L>1}>|7&M(>GxeS4NsLS3U?K7X%N0Cbs`FIu>6J3 zR|Z;+QWA)4Xe-jvw~4*HoKm%Y=is0uubkDRrS60Z#h__4adTYo1n9@?A0|<6xQE-Y%F$8bsa$^cdh;B11@~n(MbM9r=HNnp zX?J%<^E=a}2oS8DoA(yxa^m0rnkgoqgQ;L3@0%%adv)McRHnA-e%iS4jydNZ)2*MZ z%Hx1wT&0&J?N*8W#tlPeF*;XZ=tD59zX zqO*7}ZlE*2T1GSSsIo54$WRmKpwiD?&GJ1`z`+EFrzu?F+oQ1QI2|{23Z=K0K`5{k8PF;j!FZDI#C!4| zb8$xZPCVl9c>UDT-b)_og*gV9WvS$Ujeb!TNp)^~mmI}hr5aawj+wC&&y(1} z1>P+H?}%^KXxC(y5Z_AMKTyJ4mn0v1d*;@bN66;Jk#p(?*PD&I`^3rAHHsB(4*BLy z$}Rbpdqk;LX~>~-XNNMMdwKRfma09vyb`Jbw~Q7m*fVM}TUk?_Msa1Rh0`^TN$4qY z(K`P94OgC(*rHO3 ztf0@RC*U#s`HCNy#TkTP)E@Mee)tTLi~Hr4Z7q{+nL_tgr`=I0!8bSs2I5EVA=DyP zA%stat9@7iMVIZylxpY&@d?hkm%ThI@R_s-@jkbxM`l6uw16Y(@E%RdVmh@*iX09Q z$qg?TC?RZz`U?X{QP12pqf&AHvvl`E%?XlMvZ$TSl#jk=sAp5G-IlC9|NeohXIJHI z=+lmu%M^HmKJ14Vp+`w7WK`Gxp&L7udXOri(ESYZ4{hPtU|^I#6|@;e zp~z!xk(oubYzX)zvLPaPRF1iLL&QO0kWwz!&D>lTk9u%#!Sy zF|2n_m7C9Wgp^T}RqS*m%`(6kYsU4-GEtxA6I0BxQJ-qS#65l1ZH6pTt|&% zYke}30RtDrl9Is|h2bBs)Xf%ml6KPewGafA!9AFxJ?k)pTSgU?Ub6-n!ndlmyPWTs>{nLM5vLthmdgR#%7R<}znKvBk7^l?W&07j8I#2HcZu5M(N)@x&_*+@+fmsV-qIgfuH4_t`8Q6D#>_k|ej+{57jD69)>@NsYHtX9 zl`J@<+j;~CgagA~h2zc@kI4bAc?u=xg6z!yfs~Njv9FD3&ee3dF%g1o-q|hVrQ^A7 z{onLfY{$A<0=7;6u@t~%k7)Z&Z$i>!;o0-?&(cUzc@B*sotR3IZod>GlrX4vx(jGS zoPlUrHcc|-;lwOTZ?Ol;0H!(cv&B%052mpUUPwKyp(zt8Bc0_Fk!sx>b!(-)Q>m<5 zIvj^eNmGUElQy+_tD9+}pbj0nt$2Y4E{y7+fYo2|=648)({F&DdPM8h&^ua8HcNG6 zu|$uxphUo3lK%Rp+P9J$PeI+uyTj61i#!iQansV4n70q$tR<(jq9rL_O1i8?`7X+;O^ zhYnDTG|LQ}NOu5#UZA%=(gY6BSZ;wK7Vd0@drcvxam|Ii4YS965%DYfUE6Zh@Y?Nn z#mrDMolCnWVD<+zw1YEmk<^xx^jM)?7eGt97WHp|g2(?Nd`^{iy#dVM3!C-G?p%KC?f> zx_=I(?<4@*G5=sfoQ3qc;a?*2Y%tI8YMJll8*+C{;CHl}^g7+L_NdKV)EgVtrB|vELl^4n5sw3P>`Xf@qSwCp>QzYqnGOVCI zW<^YDwOyQ3LLFs>LeZI+#XI}%z@HE^GHIj{qQ>i*`v^`C-(`6jvY+YKd5KJRR6?Xb zM81HTXXq=+u$~BSzwjPKR`!Lg{Uqc4o8LGjNAq+~FB-z~9=Zdtq(7ka5Uam_G^^g; zn8?@zuRmG??VVTlVl)~?5`X$8l0Be)iY;cY9NZgTZbcc%I+DDU=6(h$fpr2Du^Sd7 zhqkh@l<%Sqoc=!3kPjvJxs;>XeyMd#bQ1xrON}}zD0-+4JXPg>Y@C&X2{OhN()isX z=;DD(IS4DH#wz@}jbv1Zgxv1Wmr61O4~pNkDA|t`^nIO|Xm~}+*OdBNW#YFhxbi?M zLU=gKi+In_i%ZahaYnSc_|64R)@4$+zUDTW?bC6Yj^b%6&X@uWzP@NY#E}RtOp?odZsDh!qo)RcB}WW+6f2|RtB?a4{T-4gaYn`kho8?cb=_z#B zOYOe$wN8&IWP9Llgl}83EqOsyM9vJ4oZ=5u^eGL}I|Ar-5 zI(R}=m#RNg2PlO(Y!P9cIGh0pYV;HNwg?%XW^@3U0!|xDG4aR0f;yO2JYf-C-MjjkK*8xPwFnrRNxLO!rC#e0`+mD(?Th%AKT+N1GnZR3?Nso?W+Oztq|6KnpIo( zR^v2rbg-f)XQkAGK?wc2dOkAp*16wZC=eNikJ%mm2z?(ELVG(T_*^O4Y)FsBz5|qg zBSSx`0K6bu=O_QtJH7TZbT4+fKMJ0!Gltb3I+$~qz#1!{;*?-{-)&s4k4I?VNsMv? zg=Vm*vKU#og>Z0$dJh3QU(h)D5E;dr7UQs>gJAL@PbgDU6`0^%Mj_!vTF1kM=a6_L z;6iV|&O<|QPZ9AZ_DYeML&=5B23I5H95qZO(66l{&}o^xDsoFi2VQ;Q0Tq7Fb(%xI zjiv9Lws#Mqttx{Q)~wdlzV;C@nTW8NUv2+$0*O)SarhPpfr09@w?}Be^v`eZ=Mdr?NZE7bg zIa^Wrl{hY{(qm>@C#yL;lR>2H{g5!EeI?(g0p7>8Lc%{Wn2M2->_sWF+5Dt^f-FH) zR*T_CjhA|ua}t1JeiN6utz^#Ts6L+xXL_@TRsl@flabBaI;$y4>>nORW*P<`#F60f z2s^5qa>Y`)(WsJPQ_kiVZT&oszToUVZt=x*#dbJht1aLA>{#1qs|E8PmBMK{u+$HY zaGY1YHq4jZ#H_#I+kK8&y9DAY0XxHlaQOWL*nDFF5_y0q29TdoLgC#Bn~5NsHefjO zx>%?rIpR9K?1H^BtHKaT*1Viy<#XsKJh+|GnuS}hCnz|3#Q|(TW!{Rt!gIza*gU_2 zoY`0AbIpM63m^Qu85LS=2$fa$$Qx#C42sw4?ip*HyxrE1XUl(Kpc^R~2GytM#_wPF z1l|z@{1*jx=vn9#ZcBXuvuy%>xer}=jsbx^r{0nWAmBf>JwgW{;UAS>4`3^C96sm2 zQ?{%XU-)%Q48jAZQyHC;ob`JJ+Vwcpf83vbt{_5jz@r)>vEPmI=xwE1$@qFH!sJl;Jwaiut8tifpXW z0iYS@sYa_TD)N@m(`kBzifmIWVZ&<;ndGOFiDOp3#m3G9`VHY(Hn{@`sjdlogo9Pv zrW9yvd*!Jdk1^p|p;h8~Q|y|eY?G4h!X_SY==a(b-Agnr-dIJ5mLaCbZ1eZdwsKV2`o70Xqg4VjF z8AmXLn`;qRyDJ@%dSIHrCm2@nM|gVvynTZSeFM{u)Q-Ua{J+;$T{=B+2o?ya0rP() zkBS%1KifYjBWlQxMzfZ@s~)WU)#i!)*Ra`5V}J6avo zzF>r1;c)W`BBEbT({B&i{DhuA9=@OoBZ0ljweE8s4)!Jv;*JYju76UrUH_0sC8wlI z8PQ8+{J-_zzNPpnkPY0u1x3@nm9)9IA`=jzN1`kb$8-LqRoyaBW%)VT%B9t4ura@} zpR%toBqBCMSgFgDGGo+L-`=%$o3ZGMKSBT|l`1&`=2FB0RYgMfC`_E0@##&xkQ4<# z-jK2C`pianh(u$gVeqByoL^yr|9Z&OhHfAdTf4FTUZ8=U!i(Xfy#<_3gJ zk7{8tF*?6rIHe!8np(N&4^eR=YG{2~-{R|aB2!e==yEzRs0tyO{xZhIAAzKTmr5J0 z)C|}Y_9Inkqr9JibsR!fPtMw-;rK0c&o#3}1pQmU^XvT`d;!OIQspDf8v??@&_MAO z?7y1RWv7|R>Yt19VgJA;mjNw2Q5W{VS6hP1&|U|V;+ zv5T@R+$t=#9$B`n3wZvS?rJa{39*}?=g0a|S1_T>Hm$Ihl-#c`L|SVwE1*Pcs`@F!p*+Qytfw z3P+-~KEs^~p7W5S9)zPjxSwFR-}V9bh}@|75SLn`A**7f?S?AK!Dqei&||qTOyjM= zpv?+?v&~L*wTm==JeYF-B>@S5hD$Ft((0KWJrAT~iY(ME6s1jE>Ku;L%Fn~-e1Ax( zHf@CV1gGtk<*soxZT_@a+1}fDY^sjakK?zFi8&V)tW12klpi0u3IxG&=E-Gkj>?NX zG2Yyx`}0KfCBmiM5Ottbo8zL@TD7I6msqN!+POBm;l)^M+9}q%n#>AV7;)IE$%e5D zb@)jZ+peoce~fQIn0*0d&9ZmQHH&@HNXfyOHuC-u6wGOP{$r>mG$B-^(3N-fgV2R$ z1v}?dfuNj*{%cH}x*s)<08kCB9CrSCLDZ(4*tu-K*`_#q`WM3mw}Y25+cHhpCrw&% z1%53m_!HmAXVDtzfL}9Zkd8&MT8TtJM-nMWN$eB{by_=GR|m zBaMA|%urprjMzL|+_-yuTBMMfN5-k$5N)HLn?3~+EYMP{prW8KgLWBCQUqmHc23CY z32)`1z$3zV4rYzcTm>>+ZEwxbvw>~6E|IAbITe^Pt*MN-y2t?_?kLq*JWPkSg;Qkr z?wGD~!xXhJLnrTd33kp0qboD@|1N1c+~ioF#^_0?+5ZwaPFRbecBv*9s&l))>R*(u zc3l+q4Ycdh^Aex(FSmeAT}3_*QM3M&!RRVImD!TP_RwN>s7lKZ%qC7Q+{(3-O%6@) z7-f}t9u_}S_6`N0wKXSSlwI&a$p&7B`iV*#%7RGLn{y+Sf}e3tg=z11;oU+ka689n z+7k~-76J{_TIWM8VH$P|rI7Rn^Q1k1*Ci_Fm-#pqV_PH;@)Z5$xpqP3`kn?XXg=P%PUV<2n<)350bBGrjD1kgwGM zYMlu|if=O<=yev3_ijqN1;PTgSfoM2t3!q5GQ4JM)KcOPca&oGK%YQZ-zGBc8_Q>x zWe&40|0F#Cw1iV^4RK3s*2A>g?dwfwECf<_g3`pvDXseNpVkdw6#~>k(NJ%+hRQb} zp2imZUj%o-X)n#V*QOFVdEtoB3%FqwN`!@Lv$}ms0RG6X$ZNCc>+*=i)-`@LmXT(| z>Est7IWB80uQb1`%5e@~R41l;Ar2_bDt4Kd1^GHaTyB+KqAj!q#Om9nz=O4fRHD}h3-`?lAN zmTnmL&Io}~`dRu0x?EIL{ncQYOP(Qa`NG##Gu4$N+lPvG=|`+%-yrL$E7C`0L$9br zTkbCa6Aof_DqN=D^4LO8U=o7E6Rfsbi^+w~%D`hKO@1)h5Ydm|a94cFRHjwTBnIJm z7(|)La`UnF1LdRo!~J0sl&cJPP31sGj=C*v=&E=D7W-xW^s-NGIzD3r=Mvt6&FATP z$oiD9F*cEDS<*%xdITBUIto?4Yv7DigXAV)iY)t@t08^7E5u48?3$>QZZQbfUa^@- z#rXgeB5|aBV=2BMT^PGAk~DH5kF;}0u6s3Red0HT8H!b9u%>!U1`93<=fZ+#)A}lz z$+hLwOOY){k(rh&20V6(VWn}4ZXL3&UBu;~%^BM`Gp!SbISo0EDosyKmiq(jJEs z7dK09*B9E?POdmtwbqYVM1JmME`(!OxRpV^q#a8zJ9$cG!6lRlXSGMPXR6Kz8_PJy zHF(_c9V8nnpPeUiQ*nI95EZOCyR`%`eb}l=i|*#KWrLb!b|p>d71NkgaAI2%8%WgH zQI-9sBu6jRD%}3?2=dUwA*}}&Dby3GFko%os=4TH_Vl}mhzSt8kx9QGJu2+^6?8>6 z8GUs&{toRMyrwTI+97a}t>7$_&+k7RcFIeUWcW@cesBHAYLW&1;TZGwB^nLTm(5s+ zaxnYk%21caiN^{sh3a?|AJacu1*AG2~dzLmCZfu67JN_mUsa@D9=u9eh_EJ?)*LLz-P^D+? ztl8tUpKo1l-U(4Z=2plD0Br(>72OPP0s9rtv5&alZxlB|bI_6$P7$LaXOS*3Nbg_k zR2{8kao^zd0ep=?3`b<5*D9cowOMmWf_gFEKwq5%0ePSDn1+4g*+Mr?Kc+DuXFZ%C zKatzQPnaTXdn{;q(5QcGH^m5}Rv7b~ay5c1n|eJ+F5J9q@?Xa;+*SYqeSTQ{k%XrB z4OZK3+e;Jc>nAK`{q(3FdSI(xy#=+6{(STV=l=D$tmPa8=!IDC{;g$1DIi<06QH6^ zqNz<&b7pK<=uZWNBq`y;oZ#+#%UvbA5oBpPRftV~67)oDiv@P65uPMZIL|jbzs9Ku znu&fguyBu2D=T7oUt9p9y8-8oQ=oswuoF2VCCggGbbcdO4v{vtlA;BuA0VSL)`WYW zw1+NooBvE=ldwjn#A%Q(6O-=_EmdlYb(99p`=ef=7N?VSSfvLC5xNW(8-Z@X+Rs1$ zvFVN%*mE144dp0Gz(!IS1Rh};^fx+k;v(KziFF8+5p@uZ&tCxqU5HP&p4&-(+ZTd+ zpKy|j*!%-U85Op{mIQn{HhS3bkH=~X6`+0@){EM}368fghh#wO$6^O~P;^hlce%YE5~5(WReZ~sA$kzh*l06|hj00S^E9MHOQEbfNY9_C z4Dl^H7PdJq<73TQUHn!Q4m04v=J3ldZyy>-2cLxx`$Jf-|GOVzU?06hg9dH(I7k#a z9haK@BkYHkxL?u&PYj6^z31%d6C5y+fwO+$eoCxGX{`=8^L1`H_|zU$bbF|7{Aqja zK`RO~Ts*b<6^2o&+weZ@ps3re<920KXA*>mY{?i1#=UhAZJlB@x!J)?Fq{WZcuPqT zL)eObbm7v2h0bXwtOZ6?+s)Q9_RK^uo6OQC>nzla%U@{=426Mg2M zpnNsxm?r^<0=vIueO%~%st5+kEHz^RpkomNnC%G|{C3@gf&lfPtMbn~YvgwBmE zT)l26ww?~1cA-Pqblo9C$h_RzAfYBa`K)2Fcj6F`TlGoc71NLhN`V@=dypYa-euHqZ)QXAPIHCm5}|VATvP-JY> zn;Fj7oU-TSH<}WZy-h^byXj-GKtIM^!^E|W?GwKc++Enr!Xf=atZe_*V>sDQf%w{u z{UGr+Q#Q?lyx|Y%D!(X#SIc{b&)o@r>9j}UO@mj(rgI9O6lvX?A?9DYechINnOrUa z*j?KDV}2qZ!Ttp<&B@$i4im2*G}VxjsTYWaH&c0?x{^4up1jK_4$i$R?Aw7bTo2wI z7o8Us_^+>H&+;Kpjl6KWz%eLDyLe-6zg8LU%LCN=E_rIy4%z%bp1iHX5)F+x=zRdggW`uxK^`2F3u~LD8=}x0?zoK zuZS@Kv>ib|uy#Xlf&L&1YZVaAm_4lF8?PBvYooe*><}N87BglYWIYwW} z4#)|LykTjayS_}$OPOFkW&JsB7_1YX|!-nEo*UlQ%qe z2LrNpMalb>A&*3_gr7$mK=$WyMt)AI z{P6|KzRMlHiBDF#a`ye&)FxHSvhKuL zL!ufpwMaF>woWxdeQL#dU8DLdK3OeI<>cILw}gEwNk#LDF2t0Xg3UQwTS3GA6%@8ztctcdP7Q9L zH|~-8$z8DvDX0gr>w##@`{lf3?ZVCGz}LgL?3m)`K-Ns2;gv!3JER^^2=~J#$}jRt z(2M;I%8z^f!u=|km3-!#%Ib$2&H4a6^EmJMTg*QQzkQ$&-f7Ymy3A`2jqk+cW?&wP zWcw_v#AF#aELn7x6HhabMhJ10qCZ9>Jc%5>z0_@Yk(A@9N<$Bwx)Qp2dth5E9!XG4 zxiF06FVP+fRL`MyrW6Grs!GH^A3V-4QIS+j*&mBx@JAW9H`#(dGDd99vx$Zdpk+fH zNquy~N*|G<+(f9?F?9!$KV9!G2n{SobTnW_gK_c5h9%!L z1YLJr@0uJjF1IRTR;ke-hrJ%iM{Zz?qO(Jp(zWr>xkMZfs(;FeKmk*A55!tc0$xg7wB*ni2^$j<+Qe1!d|_xL6Otd?59 zxy4WrQ3|K2fez9IM@2l4mPuj=8vK)E#jqE+elyH9M3}@lX*#3UVFMA{bV6Iid6p`j zm_?;|i+9)GoU}$dvubv=GRN-3Y`6U5RZG|Uh7|O6>V_S~%_}(}=bHn;4MlFa_GBq1YtB3?E)+;QQy!FZ1R z>cu3+=vOUMl=<8P_YgftI7n}SoKtga$Xs)ue#l|wWZv=feS^*mVhuScEHpo2x5~!( z=(jDysi?24DGNDyO_?H|aAqB`hMWgAgu#Q>vbk4a&gfr7FeX<4u)DS6MxZURvxa_T zEZtKiM-1)jM!@}Bm1}3SK@uceQ@v2@AUlCZRHu`;D2x=86#006eznxFj2L>pLC6qd zG8G9b%+qAk$8Z5&Vh8cp5avs@*H<6RYl&GFi9&)Q>gGb=Zpa?)-Lzs)Q*mB*+y^A{N5v4_m#O zGsciyne(!fYz9DM}yhd{D7%X*MeZ^3fU2EDn_9ufQEZ90ss(alQ@PG#KMRb@;gpoh~cLghg zcdfcQ2Wo{rq@zeDfzn>_z&VpQ+x)xAm61Z?kLme2S+-EfoR-M1FQk`T=`=#!V&PW~ zw{s#3%29<_GzQp(%y`*SZzh2URI#(lxgC~w3!t(7i5bk?kD&0EIC+S~Q8%L$F2=Kd zahc!5Z@{UkCh`ud5KL7qv;c#sG*O)khGO_cOJ-u*~hw(lY`@r4)E} zZg}^&JYbm?T(RPT&weN;fTs`n`D9_wQT&bW&?c1Hw>G#gfm8g%SI_@Qh2VG^n%e&s zVQBq7sj!X#Au(42GVyl|0RWT{ahUwMwmHHVgtSzxL$`{zN)-%=2P#77X5rF>-IlGB zWWx*bgP=Q@yzkF{3aawlAC$D%n^b$U!-tD6P$027@?F!e?%13|)TKZhnh~Wb6a5Mv z>ZU%W6SmR&v@~v}pqy7GQT(!`#eRBb`VnT9oc9KaK-frupG(YWK*+!zrMd1SJHIye zKyNAL??%#03+clq*|eHbPrpb{77i>CK^f||O)$c|r#SlT0&81|Uc>7I21iUBXnb6B z@b+8yiN|^G@sbc7&}leYfZJlXz@vn-W0{|^86_Lh#nhz_$|Z&0T^n>M^3H(n^f z$#(YhA!JQjv;MfibW7CVATD{sZpwl3C@t+b4HtE48rI_WvwmN2HofR7-T%A4@pgbV z@jv0y>VNClLt=#i@CsydLei8;N<$@rj6p#f5g9`CEfU~3dB`*+dbcJ{39vF|!|ei^ zR_eaD;!Ozet1EoLEmfxYJ`YnmD=E7Qj}=?;U$uA0X=GH$TuvMk~6Et*6ZVGg<>B2Tl+ zdkr#GRGfNzAp=;e>3dw+&1JYg#MUgQj?3Eu!ud@2R@=Rz8!q%N9^JNivy{vt7gQGp zm)bkH9JV%qrW`%`a?mm5sR@TNx}M#{lSuR4m;+%BT$L*|tzn6nTWohJ&Y7p3Jfld) z6SPW*+}4(CB`=>7e;{OsN|0khe(fH&Ef$1m$+`~s7A&D+a~G|U2?s7eHc22}mZBqe z7i<@tDzQAcIpwA5)?(L%;Ho!L)_{M6UzYi){j>vMy2+RRgfmbb?Jr>=E+vD5uksK9 z9fJ!dIz3aCH74uZuNDlXM8gnFIg5~^2*~Y}fM1L`U<*<5!yI^SS@b{eGTv?zwaCoOABE&qHJz3F50^!cMY|-nmM!0>IrPce=&H zArqom&j-z0s*4Ep_3h7ISC|x5;sg*p0ZkAePGyCIwM5P70R@YBu8#iY($O-hatq<7 z^-&H6;)4y12uHcr`J^en&y_qIBR7xTTm$<)_m1;-Yd6!kvv0TrX1f?0Ug0IvDsTuV zDEsCbQ`}>K;?ZJ=)1nJCWNX}J@#d6@VNN@dvS9QH?63coK@&_5i4+{(Ci|5}$&RT6 z=sVm-eR!Y9l$1ZNC%NW-nM=ziPq{ZM*kYceZavh#SxL1LkpyGg9PR9GwSMQ$Gd`QH ze<;Zl*80%>LNN46k@|PS9l5X$?#T8Mh`}}{tj5*c49h&}C516g3{KpL+7fKJ7k)WR zSU6`P>QEe-OL73eRyqHgmE%9|{%oi1&b#9(R^;xUYfhSd(V5E?Ll$hcETYA-#t>jU z3LOSfWtV|BwCnQ^#cOe7KSlZB-d#sdlD-xV+`7Ybe|^2<>Y0K_G{=pe`L_K!3%;+_ z8ysZiYOr!?0YOsiQS$nNeTNp<6h2QU=3SZf{7K@u;~VCZKr;5}p8rN#R^z6JKNJ1= zh(}?};%P7TtV*K=snD3qfP3R59K8;I5MW=D{Wh|M{{w$+hnw;%iDp}onbnK$LHHVE zxUdSoJ4_CiA~>8qG{_kBT&V+^*Tw2=A%{=DoS~N3c)9evE>nU=M9S8MDRlgXoVVjS z*3$0R(RaPT!(BIH44V=(C^NnrSsB7k0-tstVqGo#lqXNIu6Eo_P~PzvsLo*mwt>S~ zY6@Q?36+s4lWC25oYN$-LYHa68W+Lmj$jFQVxSim)5IS>T@MM8J zdFPG{C3*Rl#J*STzvlsbD^Y|Y$`H_>DlO-Z96Z(Q+ z;Sz3eOuNs-yt3Zoiyh*bc%5AcHtB_Z2rub_e@|>&tC>RoBQwC%-0G|(5!UZ24p?ds zqi?QbY|K4tbKARN`xC{b_#Hy=&B_CIp$}b#1KfQA72^>Fp-b|hU{rp3U!=(GJi;TC zq0(5fYkN#f=T}BzPeOFy^#KkWDJje#dZwTA4BfElr*x+$&KUpIu~=U0F|{Rbh(Wj$ zv_(I4iaFTF3K41ft@6hTtH})6TSYFYsjbS8A!4r7bT!n9m#%KcG=%k|wDiI6=5$-D z1|Qq%RjmbTc}d=*(?rU+Z%_pFn&r_+LQX82Zm$+DD(NU~QIZigFBv&n($ z)aUDrKDR{t_%zYhOSZ*ck;4AKtoB?0=`b+YMxL1rlBfIW{$g$G1L*TXRQ`Qm2Il@R zs{0sSjk$%`MOJ6oAVKQ0szP#h$6XdpzSXadeBqEY}Lj%2Ui$At^{G>1@WFDE2#RWDcp)Z zDZd8qr!W$6V^K3wxEO1*8Qs7p=eVUi5+S&7@*PKjo6tzvkF{Dd134~TckK?^vZmlC zj^mV1b-}+s;@xjHbf<%6Hs#RVUFIAFks5MGh zP!L|>T{8|=7EQr|xh8AXP&Y8;EM4vaj3)nF(rV?K)DOHr$#+zR%}G|}`v|8h*^@7A zv7ymMW%S{7n}8YtQu!MG=$YY>sJCPmK9b)EAoFf4(ztx>cbO;$j=^UYyO#(qMeEHxO?(6*h zZLX>Zs~<&;I}M=rH$^=fza z>q*D-E;498Ii`*68}U=EbNVubprq18)D=RFITx26Sbj*3_O`TC@RK%adcTMf&Qn&r zK>R*Eed%nujs-OBSPsG{p$-!u_K>p5m;Y-hejM!+Dw-MGyJEf=^f^e zF5{yoI3*C*A57}VM4uRY)N;Tf^pb?f5i$Yk{)N<~eO3u8dJ8%+1L%$iBV5Yrdo%8+ zxap%sOc^wf8l4>$ue)cfn~j!qwztCss@A21$4z!aX^*5Ro1A#+}&i4BEeRO_DJj1!`!ppS@xKbXVfmn}oQ{^$G zlln1PZ4g=}{;Hq};!5l}=v{H(r55i6(|d`CaU-wLfYOL7fqA{*7n$vd=`WyDha6dw zE;VpMIEh`FDe{w66G}twH7ahE>l0q_?8m$fBfztDI$SMTk}T{^z4@H0#>OaV@ciB6 zYV7^4%Cafdyq>V;8znts`Jc=DjS3T1Gvcpw@N@w$_bpYdyg8ox^KvYETXpl1dw|%Va+yL{x!q_QKrx?_xZ|=_Vc#aTo{w=T1e=8=ibs6|8eHZ+yAuFd`Le z8TxOLsh(srypYrCO&3EYo!3%+lt;^7AFN=(kL&W=&mVeARJFIK*U@q;ZoEI?_4pjV z)481?r#e4adM?&n?1*(hizG`mJ9ojlwy68x!t&^N;zqKg#a#CkeoNhdKWWpGT~D<{P3_vquYB44KElrEUzn zseP$Oj-T$%(YqCuAy52n-x??+le|j4VuWoL?~0!1@~$#;E4#B6JdE>KeZEz*X;!}g zt!oHWRYAPeK780TcX*yzZOBaC_24b5W`xRt3&rKj;+U4@Esb-<=kqst2hUr5bX*-4 zU~DGmD@c`JG9J9!Hk2#z{f*h(7H3YIkjjTv;go3bZr?>x&4#`8=E5pI(k`jg4Pp4LmD5;5iJY3OIsw|t# zGO}9AodVbfw3i89l}lsaWhcT)hd&7Eg<0a`)5G3PhM+F1lh1hQ_#@jTf*!oTv{A*L z&m32Mwg_9IJMgRk?F`E}i~c8bqp<_Sg=LeqCu>WWc(#VE9X}kIowd!(mt5_pXtn8D zKUvh(kaUiZWy;~U+h><&$l}E3VFsOX=LGfX3n~u_=nEh3nGb6r}|m6!)i6^6uh2PL5ZyOZLn;8HYZ7Xgu`(UuD~Om>`n~y8~?_a^(d*P$q2t z0Cd%^9%>HrYS2`keI(mabqHF&SjNE7r|U82JwL|-|8BjUk(y+qmL(_-#H+VLJD zJ$;8GaHGo!k-lyzZ&9PoQqPqlioXIlmy^sssajEXTfzD3@Vx*pRFmJdFN9vd3L38-q3}`yOHlvZ? zY|A!xj4JPx9acQoreW(0?||OF;!K-*EY?D0(G3+0@R@oY=hRh1(!txL#=O&bd7r?3 z{gZ_+ikCWcJT5*ox|YfCbjbVoq_=^;p{RdkJ*Kx+!ek=96GW=oSr)T+S+6345d_lW z06wHzvN)4kt)75DpfW(_sRnHG+)*bH%COOcvdM!e9#g+R5KiEH8Vr!(6au2sAh264 zdgw^XAK?JSh*X>gOoC8BNSc^{w%ScKD+=_Q(t|uf0|f);(u`qsm&pny6aj#e5UxVA zU+*AAKrOft#b$- z0i(wDQGiBD3#rjS2{Ke@7Ow>RBi-t+whod9)Ss!te+SGk|9vu=6hT0Zg4|Ap;M9N= z%6ZMDY|=Dk5s*8SFaUop2H%T_`1=s8RY3qmMU|@cJClG?cXb-AKGY7y4?Izmr#8cH zFMA4khK8Kd08p8h@EtF89HmN!DS#{ua9SG#6lk8J7P#jM{da*fx;qpPAeW^J-?PRA zflktBkqv%PEWlNLVXEezK5D`=wEaK7Xl6hlTaaPbbxSRqmpC z2tlLMX7!7fgYT^e1c5|oD5B^8OCw0Y|6H_ojD~6r{6#SXH~j^uCGSnif4=dxJv|NQOr9MBFZ! t8b!T+8K~W6KrV-uzBy@k&+aI4S7+~z1OiFW=mWbWLH{s(Q46{`RM delta 35599 zcmXt;V|-nG^Yzo1jhhoUwr$(CZ5t;%vDLV-Z8x@UyRrTBzW$%*{eHD)X8qQD*YqBM z-!FpK+I|N{kHhydqKG{OzJ$I1f!s95Y7Cz-Jv8B4s)L+kl@0pru5$C4=2}*XWvE7S zpkKIDbBjU^=$xex2;-3*TsKjl; zZQqNKd^<774daff>xtuB*-V&lkbno*L7n_B7Y3`)?*QITl4}o+U`kC+7NtV^Ir+v@x+1=}mn_DjBZ(YF4wDEpcdfXP4v) z*mwfrG2tnjMqwaTizY*fW+>?hy9n%J?a;|*x)r!b zel8a$wKVp9^myi3SPfl6)1S&pzngrTOiwoec7K2|Mv;M@e0-*992&Vf+(T@vqcZA> zFxmQDGUkr*?l%(m(}VW9YoU-K`Pe-bfy_94D{$KKzv=9qcu>TutWxl{Xh*{fx@#5L z&5z;EHP&8q=+=t`XLgF1+xnUcX+jwEqVhZXkR_w`QqtSfrAIM3*4DobAMUO=9d zcD;;;FRAt~8;zm%E6N6scwrspDZkV+mFnQp5c}7C)6UsF==_?x>7o^d1cLVlf#Cs0 zjW`h6d5sUlyp{{1k}-;H4o!HwAe)*@(v&iH%~U%ANJyznVn$!=JH}{NB5R|2)F4v2 zZ5uWY(-p&$9tu6|Es9>|Qc(Lu08sry~p zenSb@&7|TWDbT>$Q3DM16iM~~kV{uFA=2Kjwb1yQ(kXK6g0woM@7PU;as~ixdJvj3 z&6qRpf!XeiXQ1qav^?J$iEEdNeg*Hmpr;*KvZn?Bo`*()7tfCrNDj6+c~(wtvSAtL z1QP7pXa8DL;=r6h!{v|z*U}%3q zGAkLJ4&{Re)d#Wb(@X74eJGhjjJUBzEAj}tObO+%*bN8_?|JGoVF2610~Wd)!-rp= zDIy!DrSgRN1J~b^pxRTg{f#e2v1BoN_NIoydU^!D)u}>x#6dBeVy||C_wo--)2ja! zx)S(bGuoO~%h`j2fi)xrq8k7&L=6MuMLaKW8AxW(Q!d^P)Z0(NHp2FUegjj&fYC(s?}mzg}(-{(!?H z=ElbAtMdcc>@N_kaAiPh9MT}nXQbu*1YF5^WLu#(Y0sdrpsWsF)+(T$(M6b?0Bh>m z27=hAC1>$8ZZYm~DINWk4niDk1$D{0_xznD$;ROkFI}jsE>(zgkw^!OaAI!_++~3uw`y0 zVJXjuGlqyX2_TqiG;kcoCgiNTzz@^!S=~O2?76x>N97>yG?{wGgV-PV4%0FZwH)fD zw0D@;2$4gBn0@1K55bSY_rq$0Gd{SIxQROYe$55{~Ck4buZxw$6j64YDEFEdJ z;Cvc*g39!R*fxwu!k-fMXvVBwg~f?B4S3vGoV#u#mEUX6K(o#`9*!Il>%WWv=ioDj zjHIo029^OaYdN*V)&Z==Oa=S=1d1Y6b2KI+L&Qs&{&J-nokwsJDk@g@OW5N31O-|G zlWv8U6SLN6aOU?3a+uBN6eActMh7%|4#}T1_=+G~#4{P+%jY4-rv1z!NVxjd++Qp7 zcqWC<&5l9m2HISAtltymnr^rMOnxt$8O@-wMc)TJMQ$^_9Yq%;c?29u)mLJ6BS-Z7 zuib1aVbXPygnrlpZ4~AWG5qxf;hQM2Zv3$^v2AJ4XSjZG{Lvx)?Ig`C%X-zGRy8+1 z=l+}Nl|}ZxK|l~4ljP=ha)8i`SI+`cXbfw)^1^59IO{HJ+~;-wxP$>r+$nf>@$T58 zUSW}VIW0s_nL^c1Nv^EjL=7qF4P;`Is)9!95lYocVcF0JVj z5t?Zbky7h<>L+5t93s9JzDL2o9!F1v7({5tVnQH*h(6iv3=U^fmLYi*th#qdEPla0 zDO&oIRGcCG7o6cFhpi53!Gg`lO5p z<>s~8EEra&BJs!gpqqd1U_gVLJArOS2JC0HXVOIwJ01j{>DF9aIH)9v7v=E_nZm26 zF|3Gy?SV@z@|xR?(U2$aq+eA$r>QonwI3C6ZnKiRlp@F5X7P@oyqbh0O%m@(Qi!@* z8si%kecx83bfmGKH)fgPB=L~Jvdev)5FqeP-SRhv>`w|4ykl(dmRdAuKcyi6{e|iM z#=Ary1-8D#bUc!TL&t4M>zC@D6}C!i-wZQQ4kCBT5-g`eOKf-RP)x-z}SR)c(N$b3jzM<0`+6J*!8(E#fQiOLSuet4lBrT~ z&36eE!r%TPK%+;VAvCwy9U0deBBkGpj<-g}m?REMjOvyz(G8)QliJ;Hx_cz_m3y~> z%WSfXN-WycQmqvZ;5n`GPv4BC35ktYL-+=2GCQA?NS*eWq;E_uJpvgkr@G)Kd%*rT zKTwk22k0Td!1Q4L^IUDDWJx(CWWa3oQx9}AtS>%Rvo%*y?WOuoM4{huN#Z~1nma9Z zP!kFd$(sCvTV+`k<^xA%v(g(?p`QG<2RAh!J!V^?#++UbV0ygo31{{Rcerx)DQ@^# zq;Mr`7Z}ph-!^=5w?8)Nw!dBum%-TgzYrM+`PyqP9%p`M@DIq*Ef7c}hXRT78dD}u zL1$;F*pu2g6m648)!bR%!_+7;2#!ugO6h$O?bn;Bx?Y&) zn${y9MZEfd2C`I&bIek3u(Xf1Rb^r)#yYO&bx3Y4wHGzfmER=HB;YWD4Cltyt^8x` z9YN!CJm}*q<>uF#YcnRB>Ol624DBr53pKm2^|ZfCRZ0yU6ajCEg%$0(!U0Bil|z_+ zk61=hDdgM1X+&uK1O0u2zk^=7AwVHICGR!sRsG4ZG6V zc_NbNRBcw0zFNbhTHy8^8(fvBvVrxLJa!s<5rCbEmKUo%gf7`9*n0v0v2=}YpV=0{ zn1;T@L2bU&RGQ=xpLb;5UgFLlTjRH$;@U2%DCz+sz4hH(G1nZ?{`LwgX?cyQC0CCb z6BG|}iC193R0HvCp45r?%9a&1Z>Gy|GI8iw=@}*0I?gOxHIPaqwF;x1!8+by&_UO1 z-`m2(2FV?bqA*d7YFk5fj8VJDjGR3m={6B5xo{Ue&H8jL)L|B-Y5wq{C&9vLZFLCG z8)}NgHFHuKWX1;3yCTogx!`o6Au_eY+WatoQKZ6c0HQe~V!erS>zRnDMoFuKn^|z? z9l<6emHcQ4N>nv)r!@+Rj7l7>&6{(i#2V#{uKOwOgquDAYTyLF`W-pCx{ zn>QfL3K-A*YNtC?V9ZQEY|_LTq&ApvuJyWpBn85c=-fSHg)cc@Qxb2RWbl0CmBTFP z*ihFSH`mmz1YD(VLUD<`)8?Qn=9IwG3#45WGYNfd4amkjA-DQ1p(&L7`Y)eB#@`tpSD1Hr|R)Ph@*Da9Bw$L_zMgCIBqlq-@IQ=wAN6SP~hhox!`x3*;x_Q5y=;5 zT`a{~&)Pa0q<_k39POgGaJrK6*`6mIstxxSc=!a!kbYwiQGxlcDraD1#aFxs33dk8Nr*$b zG`|%*#Eiimqn1>1=%lS1V_f-FlP{v%1Q@SSAb^=8e5R`TC*8dxSr{dj%(){#w=%z` zi6HN-u4ac@+@kLxUwK)1hZJ0mP@V@qQF&IWOY^jw=`E1-?lhg2?J{%7P48EJ90#7H zYSfAkB$65j1q6PfFAhi18iY!#7v8C=&x1#)4I-<$0x`=>Vx*`F4Gu&SXENg|2vJ)5+B|Ehy=OQTMK8Rbja!X6nl3t0F zNg?2M5(4E1I2wc888$Z-+5%=NX9rA!P=ga3JT~>p8e3%6>5?3YDS^F)&$T^Y`8V=5 zPA&=~&CTWM_*I4JIqOaM1Zz30x;1noR|pc@@^mG|ZQ8BPmP6{4>j`3)L7*WxfRv*{=?vM=hUOqo zk#S0qJJH7wZHTnL5UYZ5!=7N257txCDCYUe)++1?`|^web;N-s`dr~|oqA4jUXiiD z7=C{K*B@Gur9%gupx^$j=?CfanGT-08hG?oW9ooKce=rK(yS7P)* zJL*(R2dygB>lnhC#EUKvDK~i^q;S9FBiQ`dNW%9cFNYeWMh$Tu9%LP%+% z=)eGhv1fOP!_-PSd7qpouiPi+t;eTa!XB__qrly}(NXQ!f{f$WZb$gD=<=&lDK&;Q znBqach7(Kox}UIVeP|n)rW=RV#lS0gziqnJYVL-9It|iA2$6=DK=W6em(furKii;I zX1~K8m3mt64ZZ4^Idx9ZDJ+rP@Xvci$uwis90CGDc^fFFQvY%o6WAIo8D*>*IT=P- z%%TKlQU*=pA{}$*ofSQ(9xz(ojw;|Umgh6J}bA7P<*BCW+25KsbNB=|G8 z?|1bE8s$8~vZ*=tS*PASKAp-}0XIYvE$nj!*<$tCJn4QXI^I zM`r%-OiK1AOM(qpf_#=}0^q&!d8B;viEczsyF#b+;PP~E#-c`*Ka?M$+}=nMFG)-y z9o}?NA9bEgj#Uw}^6ft6{DV1(;tgwvKO$q}rikKoG|?Y~P@Qb8$t$?cf+;^y*PQXh(hYT7Z}DNr%8Uo!N!(Si6r z4K^jfq?9A1#nLIQ+%{^k#!L%ZY6ptE4=_v`tlqSmjvuMb#i${aVRStuyCvX+IuXKz z#GSW8?01GZ*-fQWeA$BXNpq3P$Xa=X93q9i~s6P`*aRUiPY zaf`iCL8Bi_+UDfH@Q8CvGc~=S&+CYow*T2!21@f4s>xgt-P)0cx=U9_#dkYL3$|tb zG5BB07eQi^to-YEX{i4;2M9_DV6Qwb6AFG({0vxN>JNFm_A??OS{-FfF-4+pX!**J zrDdFscit4Z!OI=>T}ouC{Dr_?xUw*RFv3tk*iUbf=bd9Oha)fZ%ge7%h=A{vjL$B9 z9|b;`1G>8T@xR7u%D%NpViY&*K;A$-T$w z9Z$x%Uu?A^EC(6V9(<<4zG^1(Z!}L_<*~Tra=BW4+dp2u0$Va-^yTGQcj%ng+2F;@DRMY7*Qq%xzJiWD zNZ9uQo$CZSk`?ac{T~00jMP9f0i3Tr2fdnSjw`A@{9)H60LWL)uC0`2o=;P1uvI@_ zR3}4*;M*^pQu40fgeb;Zps%4*7q-AURPyQB08&p*uUHHbS0Q>TWN*e2f_kr0^cpZ* z3QN=H%Iw~%$N6l+k{?^)L$$Y%_A-$3H92BP`zPS8ZZZyX^w#aNVOgX}fP?WnW9F29 z-@EaYn*qw7fk&h5oMpxWBA#e?#PktD zX26m3p%kVpwm5XlC-p~p-nywIzzH+dA-lAnQ6R7JHUR`hYwiT&fgSm^)$^Bo4vjkI z?JEzN+cSO;jUW+@7uJ#sF_RJ&x}@}` zpErki_vC1F#LcNsW|9{#}G7f=roDaI2ICVrS7#cZ#z0KY!GAOnz|`*toL9~<5F zC3JQ3Vn6H*TtA8#WY^LnkLON)vN0)dejENKfxi+nxNGD4@s zVrvEp6bVDinItjR@u~yh$@(FZigkzWl(~1=MLJ#)v$Kv*gd&745|O=1Tjl%qPte3J zh0=w*G_2FFm?(iuxW{*l2!zx-LZL!spA}B}f!G$y%KrF7QmgN+SKPgZZCjr;%cGn7 z9ynNt<>Ec#9lzo^cFldxJaVA6eXYZKvt}j4dy?03?+Z`+uE=b}d!)D)nB4XPhK3A# zp<X-ppUpEj+wo3VX=27o?w1;c=&Qo&fb~4(ij=ZYT!>AnpFz1 zBb|}Z{;@PHJa9zikE>7Br%{#QzewOt7vn}y^acOl$iJrlg0D(S_@_|;s5Ag{QYIYS zfAl>(qHVK_7U6AnV>5r~|LA+6;T|&KpD^YrIx$>QtLFw55A&&X7MG{b*N=UQ0IvyV zUw%$crO#50wk8U29fO4`yRK55JOdX#f(|3Y=R7(}`cMi_Qp&b|lKwT+C4JQ=5eegqxAkR7Q_XQ@wA{cC`-yR6f4_#HByBAhE90YZl7x51q za;Aj5SdC2JE^w0|yDr6rBjc=bh}9J*LOkKU&~1Rxd83`6Bd3j)>`T$9z*DN9)&6l0 z+MWq9TPa>#{)41h4)S7exxAwp#&LhW!646)Y_5zi5{z>CeAp zw}uuoskR&$7^9}AgD!#LKMct+&?u>DOMAw?hz3psg@O8R8Ldeb|5M0LR3>bbNNeTq z&?b^}mHzX6`}BU1{^QVkv-aW~x)`-&td>JP|l-%95&f5EDLEX$4k0G`WWj5JMAa-*~`AYlG4ied3J)tPG%i_lR znWIhZ!5HKT=XuQR8*3`H-Nj@gzlr+?HygeZxbyp(i`=Soa_kLe1-E%sO|pKpKBh`E zEv%6JIW5zxtFwzUg;QgqPe#Zg6#5l533Lram48M+MLG?yI+M66U7XuSgL-35VsxI< zLwA=F2+RC8cOT&&Xs<|~nx250RH;qd74-LY=FH8UG8+OF53?jCiJ)_VR!Whpy+G;i zqEZ_QaW2#7v22n&@{Vb~(r6pwpm7V?JeFD=L^=1x7JY46PiBZd`&*ZkjzcE>6oOW( zwWn&GyHvmFvE?VmzkC*&Adf(zc*`DCA@oOJSl;jSJg-b-D40IZ@FQ4a&k|H5OC#9q zf`#+%8SKoH85os80Ffwq-F{SWbGqlC;jWBxLJyjtr&*jKRJ@p<(SCD>PN1cMiztKb zYW|U5>Q%Rn)_%zu-AT5{yAYuMj+w$85$K{Vdez^m#;zoV+#JCd9m;sUQoa~NaE)R> z6yXBI;_Q>Mq;W@brlzEpO`qRWj5pm14Rmq1sXW9#u)o`VFW*89PK>~4CvCGTx1tWY z{9={=V1?>h2&^8)pYF2qJf!Ph5+*h%nZ+8S$|7Q>-Y8h#WxTf^G9TZd3h!VuvNDE| z9y-emPiz3hCnH||d7+lEyK)i|B2g#`n_tm!-RC zBoyc!$5#%CjANhN_909WnWV%KRTU2#QAV->1Cn*iA!Ze@m?8JBxE?rgFN5rUd1g=s ziN67=5|^tB^>h)Fd5AS@qoatxMn+D_ul)68%n4<{z0OF)n?9sjqjro?Rv343>|I&uU2%?xC}DxF zpT^2I0Uu0M0j5@|YtjZFj}NZ^@xsx2cPsaaT!>jyC+t}bPE94bkEz>hi@}A_+AGD%ji8KKH zp%Vw4zioPd_i-O1jGZbaO`viaOwP!P(|(Hc_z&UrtKdn^g*QHXWQ{2qmQ9gya^&{B zblmymGR18fjlJsN2;4Lv$D1L8fBpBS=8??2)&0{q6WIUGE}qLt{|VZ=+3 z?o`wAx1u1asPsA>&iWjVH`uy1WJaT8GEQmO=!87?g(>-zY`bBb=?hqQKqpvBtGeAE z+7ucF-R^+2?(m0gEK3yEOi>q~>o{xw9X|Q4=W?rxNXZzap)bisqp@eE3?fM_ZnnlkbZLd`BGiSoC8vDpx0}OJ6_C zpva5c{iTPt-%1n5oKrgHNUR2t%GWQup}LvfsI6@Xpyk>)Ol{E$^XOyVKxYiYy6~&+ z2|~IJ%PFAe33X1$A7$D=Yx&E0R*<|KInpiG&)jK)STfXMn$tSGgPYr0vI78_O2RTD zy4_Rl7DgKZAu%Cmw&V1(E(MjC;VewEFe$Miz#odii|o6^T->Y_ge8Z>P;a22p)Tz} z0lQ8T54IAnyt8w+RUn{HFC~4HvzfK+)B+D^%?{S7`p9O<8*4c;zqQ($*# zL6JyD_mtQv<{ov(>yi%&!3K?gktLA3s}E(Z=(Yj>{4+uu?1o9`P*S=nPEdDtd*$bn zeZ-Th)bp^TH&bxje0sy3p!1-aCqOd8c=K=?-AA*XCgOY*vDQ>uA=9KzlU&kX>2b5| z14z=if}XL?;;iA=oO-Rix!N9DG>;LY==3EX;e^%hLO@v`4DFp@;umy}J}5-Y`Z zaR5s(l-wC{_eXR1OOQ4DlhH-q5N;58jyUkFNZ6#{QgfufD}L&3A%GuSP(Qpw=BX*l zTmm{wNWv-Q(pq-*Qs(|^XE?r9e*^~&OVUmahei*X4RW)BQ+}Gy`z;xH5wpUp@o#59!*sVAyW)wEs?HYcgdJjd8-QACdJ&(xXR211}+YSu! zK&}%Ei1EaCyq6%|5~Q7cLuPayBIFskU79Zt=m~p%Kp@5)<$$Ktl~{k znH)yrT?dQ}+65n^kaQC`dlr+1W^)9hgGIjsA^1j52nabAdQpAs?ymf57(sB$X+pt7 zMRoEKmGUO$5~ldm#@%he)@?*$oR?`c*uXJ6bxpCfwfH~k=KGAz%*+_n1^qTw_+nUE zi6gAmF8UUnTC4q)f6jvOMrTG?zw>OuD7S@q1->M(LU9GgJgPD{+x5BF9Gd8KyoVqe zm2fY9V>Ty59gN2I(}cvN0?=Y7J#KvV((u`8A88gCs7>qN4xAm#Z^@3O)9@+6m zBVVOJ!MC;DUPeZ{?Y9Cz>;U6#qP6(krN`9El*eP%$JN6)5RCTsDC5RjyBct>(d$~W z5Z_`Iu6-&C-Bd~-vMsj0G1U}bs1>7bM;aJkH>^f?60wtjLpr8)d8--0RYJIgwpAm> zCE;Ub#jnXJ)?NQe-^~+l08?xLbR-=C&?si-TH>7A~ z27w(;qeKg^PDlJuyp^YYr(6^+=6&oiy>)Qh?4a|S>faZqmzRVv1AQRBa6g&kfPkOV z7h%LR&_meekse|MF4+_VKE}mu7fW+(r75cldB`hU#?aJ=;)P3z-CL%VlM`Dt`H_5> ze~0v;k1|&j#3(0=)O$qRhaDYDOfk%M1qmLw#P}GQMj4O1$h%D$gJ~Cer7`>So=kB0 z=ymp50wp1o7=2?zxcZIeZhpq`NdxN^7Kj}%A9X&d>b(ifo@Ab-?}MswSk;|jA|PBi=6c0YaRE6a~%@zNVF^d^z!Sp_lIjRALTJM?2r zP;Z~}5n&=EAJ+B-@9$>W9bJ zw9+$1d$6QCPCA;0>3^=hdmgwi`N75LKAGj~#LP+NzU9ug>I`DOEB1Q5k0{#D*cq=Z z7mmC5#W#j_POjHcDi~oQ9srU1A!WU>t7k8f`-V??4r6N_!v8e!UDJn5ZB89U84 zb>r-gpzH&U*P3Eub`a~%w6}lk{P^$4|u1IeOvGREdVi=1fRM=D3-u! znUa$vc2dXq4p#YWJnYCOx)Cj_Jtmd9(Z%Gdy*R9~Wc}%LkV( z9US&g!yx{T=R%3d0+sasBe^7@gZ0#~gICB)X+fLShNMVr-G;KVj5s)~ep&JkEf5V2 znX9YafES`q_#A2Ch#K-w%6(cWHp@NIq)|SXDZZx(->;iPngFCqMqhs34Ld(XZ(Y3j zOBkI%0A!3{p%gi03-h8;;^XKp2;(Me<1qd>50J!Lf)y^IWBgcpzO)p{vCSGOQ4ezuZF!rXS}>{;>BxC;Nq3eP_o#H7k&Wdzxk z7zxlWizryR?#?f*RTfj$Va}*H3EtHbKPsB2O6bbR^PMieQ0O~%{SE6^pAVjkTMTCm>*Y>?GJ>Bq`4 zZ$WwLUtynJ$b=f@zxvfc<4miuKFb48dgV2ra!*Wgp^3g98>hCc$TCwqX`M+4@eov7 zP9}b|3alIef)9b2Q^A(<(p~ocMmasiC;JBkA!Waf!LCHt+`RgW?w0x>D^JR_@j$H> zY=ZXk2>9X+>@36Tb-%l3!19phJ7^Gnq1`5i$(~?;3VUCC>~2{r`@rZq(sd5HaYP`XwdVGb4V`~IW3I==4uZ06v2J} zo^>&)G4rgvGN#CqIxS`K7u1t&I{3JwG|{}u5|)uZ=-w45%b*ocvH^rU4lK8$$!BSBqRPb1ifwC@op zhfdH=X7i>@drLTYv)m%texaXBCmL!HQBlFOQT}cDvRLc0`^!-m1`+yDa$9_2BGN~t zo7U4wt~NL3CDD#wCVe(Ei?#6%S@~OM<&|v<{WSxBp|GRl4m*rc1F|W|^cwwt5_#5o z&ma4*N(BE`B}oZIppwJ9I>zVa+(MO;j5IE#YW!jWEu5SPLP%vGk~%0EZK+I1EP=cZ z_if1Od}Vh{J^`ikdZ<6g(+GKeKPO>H*=}wKJ*oMN^+Fe-AQNmd)%V{pb{z8@`*s80 zAMR?vQm?uCJ&*}1HLlX_?Cs#T+0*1IOr|o`XU;na;x_&O86LoO6m{Vht`%p@)56Fq zXez51FGG)>+KRW~NIUmc=OZfg?XuTMQeSMvgOlra(r&;1-g^YQm>ym=tcFJm6gthL_zdC@&MwMm-mTtG3F=Pz8p zcfM|_&yfC}f|Oa7$G-&|1%~h}I@`teSG^nky5djv9y%%a=&EoW*>Yb2&P?~n~Tq;yFpKr@(9gz#=s3)7K-)j%D<#ZjCW*7?@!j2cL+9}xJ>=3JdKU{Nik2NW60Mhp;uZDiZ_QdtpW>o|_trA?1N zdaFBd?^B74^nm>l9FlS~{~FV2ru1j>T9S}1G@IRysL-l}s-3P-Mi(Pa0w(`PQrgzW zA=nUctUf6X^oxnE7sbt#J2jQpEMg`0pe-xD&dOrA887EdKY8kWbF?!Jwb=q^!^IR# z4bd*sHxoE)yE{1M%!Vj;S$!TstENmbjX^K6zk$ksm9B1^b1aT+JtVt+KZLD)U$1MK zlBbMZRj&A(8uquo7)n!9sW^#e`N(yqx)w%?@|BREQbnAjgg=Eh(gm`&=sxc zG_%0jVMPPuC@|=eAB|WzCQMg-uf>d=Xj@6~kwS=zy9Af>6Ju&nj1x1CwEGKGLh(E! z*2nLI;M8anJ=gtc;8`3=T8_=|KaaC=9#|= z3ysm!1jfWt81r2rKAnX8f95|(=>CRxfv5amV)BN2dVKURF+qj;|B1AZ$!lg-}n`8^8FpmIW|dc{wmy!JM_GCKXvc>Ex&D< zOba|S2DovKJ09%x<_tk#LP)#*vH{jb7Dt{KiusADDikk-7?-vGkUe9PcMYa!HFnm< zQ=Tw%4VMU;be*fT#8E{fuc?t743;9pR1J*|l*?QaU$!gk5;utjGGn?I~sM5(VpxXZ`j5>+Qd&nCyjE+;?pbcd5?Dt*^zs0u>VyT>o@f6umGzP+LF0;Hd4NTfxCXZMhnzyCc}8V^PTC58~hxdcD+z{8%@Vr52fnVmQV zXPSpuYwMyTYWy%Bv8vn{f=O}h8{6G}u6DmR{j z$LJKaGi_9mx8*bgG;#aIlz@69HJZC#;AiD`q**$a)uSX=$`eEqo~yk1F#gV?nB~RV zI)KOun^!+g(4ju97*~J2a6R9@$Z#*iEINz}TT?x|CR%cmUpEL-AJUPkKT%7DppsaB zaSvpCdN6mZ)oG{24HtjYH|Y6|8t{I%%vWl9t$5^i!!+Lf>P<*L3_Kew6=n;TX&4=M zuD@drAYIOv|65_9$vys*vOXBW3!5OYw_$y}&tHk^gIk+w+k9V3cI#5D8z$|%Y!Zw` zU|ZNVuvJ4kw-JWL1&%X1k_>BZ^5C0vqyoRT8Dfr_LIYS*V~B5q+S^i}XP~8d66(tr zbKJKk^wlKp)&^vJ0pkiE4RQO+LnoYWt-`R@>*iq?ejrrY@#_X43~KQ77bUL;Tl3mE zCi1g)v)Lgj*_SDGY(LLXCCasH|O70!c5mQxt~jN)>#P^Y|jhkoo!Y4?Sjx&PLx$UopX+V+sc%EdX_5+~** z?YSEKp^C(VVO_zrI(L|C{7aX7SxS;|f!@~bDn2ic8Uik7vBc|N+PY@rkJz`LQ51bh z)tSJ(8E;l345UK6=TE{#o6y?qvzNDu6xL-;A4kBD?`6gwTrrL|%<`^?cQ~Tq{DY!R zxqeLk`-GK41*SY_z9>7mdm}2?6Y8NhJ$TB<4^vN*yrrH^lwee5T}#I+%`NqD>hX0!7p=$ON?dF2*WvJ#9r_+<{mBc7b;DM!S#^v78R^gdp*y6 zo`IqMJ&OVr!#z>EwcnThJlWtrSi$Sx+d?`<$^A_-%uzvyq0C{IQBQjW43~f20QQ?* zO(hjoHo%e%i$rBF4F`u7+c$$8d@!vN9!+&ttwmXaA`qE`g)jXWdFmud`mclRW|2dH zo3seNWE3A!+j>q0WMkHo89EGpu1_I%{|rrI_^&?VQj`;!^(O(WKJKQh36 z#LZHP4hnueSwhXmGO$lgPHUcSRklRgwPBu2qS47)dSv-=+mTR0QP}5yYsH$?;q4LeqGXcVjZo#gafX<8q1)W$@pT@HekrM zA~-s8!!wVC5>vhi+UHMmq_cWvQH8ztBPi0i)!W#9|lQJRJ%3FO2O?3 zs+C;WYCpdaOcy5b-ZeO>91#8s$xV3cKvlI;`XNGg5|U6MCD_*`J7A_uFZo=t3e$^nq+Kj-rwy zzk9N0w2-9Q%ENS;-Em~8P2|+2$x`Jlf4t>Y1J|4op`5U!9EI> zU#d&-Psw#7Qf_YpyDRYBKpsj&P4-1iG>~q{#j(yfKV`RMB_8S8$n(OqP0uvb@oU=U z(gACQ!9_FdkAHSQH`L=g-V#~ub}mAu;1_3byA0Vb0$NW?+5h9|oPsm!x@et_ZQHhO z+j!%S-7z}J8{4*R+qSKat&W}3|94KEi@jFWthKIo?W$d4j`@tDwQt+Cqi?x20{B#L zJ!7Rlf)wk)VhYziRVAObZ@s)j@UQgBil1)`qJMk;(_i+A4@4l%VL(7=QUB`?RA~zV z|2NP~x9yv!t)i@MszD92iGVE=I0RnYCx%c`OfJZ=xV?F`@Hug5kHB&#_JK759!>cL z0!;ebVi~m`$i(J6v*mo#?tZ-aJZt!)%0y;&`^>8GN%BV4M%cgF;apS6aJhOeR!@o2 z9vj=&674>MKY``1nc+ytGL;Q-sc#j~;i(8~v4W5%go$jB;ONxewo2zBmPd2h_w0B1 z2@A?|as5af=qN&15+3&yqRg;fdAZCrU3KFrh?!2`XkK&Cn(h5vZ9qb*FT z3cV4m7Wf^FZspT@W=JKl%{qGs=9NbejhZ*9OP%C7F$s=Xzq_@W=B!lZ9%3OmKN3b-`A6dm=yh~bB!%VJi6hK2M8oWB zePblN(?>tJ#YyL18L=M{J4CF25%65USEe+tN0h~*JF2PdJ-1gzCW|?t-TvQ3^pm%| z{!>W*6LPAR-;&n<2{{e_SI9X9@Brbw@y(irES^{z6R+qplNgK5WOO^N8ekaWT69%2 zv^JYEo7fS|-t*+$sKytzv+MXaImW(O@_XIZJr4rcL=I0*h_KUfU+U|BQJ} z@`@#|t5%`SBSk5MmRs}g*@SFnPHa4CYZd_PEv)pd8x{yOJnPz@8C>lfDN7AH>FSh! z{Oqr%uqng*hTR5BRM4uLtxO-a855a9@k$46Zxd3F%hW}j(^GN?G=U|r^3K1M@siBs zrd;}?$SB=Kx)Qu;8S-dLBpOA=o8rnrP01vuSSiuf$TL=^M|Sj4kOAVp$uoub>0N5> zrX7CcX1Lv*ycT(`pmsF+WecdVNd>MG>Vt><=>oU$=tE_+1&YfpsVR%F$tm*}M#;X| zTKPud%eLIqSdq# zx@Ha@id+_9|DpZno}FV*K$i>fR5Rys@(};mGk<1;z zkIIrg%v)ah$yBgjguF z?cYaO1QP1nSqc}me0a?bi~tzAhCv-QrQo1^4BjdBRUm018`}7YG->3eJG$uOsd*nH zJUahRZledNDG`CXtBM+7We03a=%5X?!Jn->9NyY;#}6Bj!S;PHjskd-507c~70+sX~ z&?5!aHDHAq6iJPID@o<1=5!0pwX26D`Ax^4yT@!lEYAiMN01W;{Qe>nzN z#iN#KY04{8L{j$!KJ|{kuk3LmdL)zg%TD!<>>UPc6;sGnh`8r67rO4$^0VduW6#PR z2h+=6*W-^EPi7sNR&uxWiZ&(=X^1Rh?GV1#D!?_t(w2k;S_0r;eAZFc2|)RXSC=^BG^R&nCheEvvu~{~dytLH!ARF#||wSi(e_ ziLr*^JD=bmgatM0pQ_5(BR^aAK=B4!285^`Tfux-->Q?%x5!@*=dB^yo=0K|yyNYD zaP?9=J;xhJ<+D&bX$2nx=F4y_Fa)#DECFVmzjW+-#b)8`dm}-O9qH1VJ$!zw+whzR zP@nuNeG=CkGTf3As*~G7im!mf&IbeC8y)_d+b=B+TaO|7TvT! z%`ZO^!~9-2x=%1x+%x;aIV_JeVAR~L7;?a|u1m!6sq|**_y2hy< zWk$srgt3!FV5*s>x?7_ZHF`wPhhCS7Bxjl}(^_OjpL3dlyFEASte9w+Jl$9@=x-{f z+gtA%dd`_*5LT^nJq9NZE;{cWdLES_SQEc86L{nz<`BqHWqU7O9qGkJV9IBL-i#bh z;V?ned_hH)rrAi?-Utc9I63V25-cq(4=WYZ2?i~bi9D8bD61D{4%qXXyWWT<=KzPQz zwz*h;vAaT`Phem#Vl$fA=33xrj~wkBxXkU`DPV$m$p*j5Uy2R_EOt0%AmwZcxO`y4 z*nz|gtjZC*JPJbyTQ?Kg`=B91xG@4?(i04t;|RRS!ee(u(r8PvBqCBu+e%-eJ|zp{@O@yUi_Gw)cQ-9{QK3KJ5*rL`->qMYvOM~5X_{MJiEpAV$c5i4hZ%p7iCv=4Z}?v+S8(!?%T+1rdqWNy#Mc9S|fYF$72;fz5>XB?Bz z2=~2RvkdAm$rIsX5CR7ME5xHqMdWc2>cmk3b)tTKCHDJ@hUqi?pxiwvjknC?R?u=bXra||nznZWAS-?7&{i}64i=F8V zu6%KhCL=lQ(!_Q0bVv(EPubn#2gN2`6&?OkY(jkVuiC-cTP^j3*GgtlfHS-kIE@Ho8>Kc&gGSlt2AZOXi0Oj+Go? zW-cCjMq?c|a6^$xpr4YI#7l(8kkHgojwXa{_Ggp0<5}gKDL(xj>W4R#A6=(>Nlhu7 z;+ieEmv#nUDBs#k$GL6NAL*+d<02o^R1s=WOWknEOVaBpP4=QVfx$^U1-DEZU%_R- zkqYJIycER?YBVf}QoYHM%jGJwMo?(=XPd8R;ZKIegvNl0sCH3Kwm}qWnuTfGqG7?) zFaPhdyPH=fdAu(oUs|@3M2MAaQQd{HbFGMrj>B71x4yHL5bW(Lx5wHKuH^3lrqn6C zPAFr}q?uka4b#L9LGl7%CI~Qjw(J9l1k)JF28r}2i%LSk&%hI5prR?LGjK?X)pOuM zfx{Man1a5>l76a2juG4&RoaRh5?+*t5bi*?f*xb++Dz}MVHGpRm_zOq!VT(cpo{Bh z#Xyl{5)shgu))#Ju9Sh)nt+LJF*TqCMaNBArm9?ch_>4AKcE#BrUjtxQKttc4pFl* zWJO^6lFx^m+fBr@#blxcK4E(3(Cg>Pp2*2{Eg64zD33MNK8TIeuW8Q7VU?>U2bG&5 zo`*oQZn`ZpRbFb~x9I^$;KTkF@zM#t_4UVOP+aB^WqtC~P%p!}z|+YY*N2G8Wi2_K ztr2fHbq-=ZsTTbs9vES=&PM=OEvMnyWvt_Hg9(|BUM9AJCKqra3UeL>3goz;5TrZG z;g5XSsFeih3ZEnS<|Q*h5xeEBRm(*Ix=to4uoQ9^@#JV{gCiv5d5>n2Q|q-Uav`i% zfiCT|!4&W^HECfIwJyQXVYF6gYNMv-@)1lH9j;RLl^n|I&PKYEHNrqreZ3q2FBqMc zsTLM;eE6Fux3yTRmXDSYiPkBoIH?;O8$rV2dlXI)#XR#EjnnQ{7LIZZ3w0;kK5 z1-FtB5N{;!Q81~dAQ)o0ccrCsKXbD>KP&BFV)*m4maT%_$UoTiw4mX(#1=q3l_EOF zhoThb3Pe#X>~XLs3UXl3jWNLW$wne5(6J~D3`M+~nq@&l{xSF*Hx2*p5|(0#m**Bh zhKe}g(Oi$vjwt6i6`8J1S`GDcU^_&)kDdi?oW4$B68w!#k`|e5-;2nU4q#o|Hpl~K z^A2GH6~SPhZ*z*Hn7J`OUO`7d?|a&8>4KLs+j{Yt8Lh`_{|DG61Nhr89FQJ>FUEJ)FAtTlvB^D>&Cc<2e!OBWxuD>VK9E**NrpUpk<{C+gig`9{PwTS%-;vD!gmTpn^rJa+*t>~LKt~_({ zdDqV`kX>3EqEBW)*!Q(s^$A%o)XSsv#}?IZ8RWAC0dVDHVDtIrozp`V7Xxzkj;Sh4 z%tIye+3*|H_GIv(u+s{~5BT#(r&QU`EMH85;%`9=P+a0c*rF#o&$nU63e={d8&ZdR zF^CQ0SU=#Mwlded{=vh~r5hgY*9(<5g&g19QlaqfQWBS(FQ+VT2@yL^gUk8UorjKo%eH@-b#R@0Udd@CcVmFgoGvyi4EJm6E?9g1w1I3g<`)VfToctQtfBs zod$>{2DYd9-_G$HaJ5T(ljh%9*B~c_dMfFY_gg@;%gOrz85=c=@M@nf>e<#xD0p-o)g|o9`+2(4HoXXOQMS2d-|9$L;~lfn_)(K3S_$K3&F1OhRVgThzXS zQ+B}L-dijq+1wZG8j*7W(u}(=+i|&~f;)mE+l`kqjhcFSq@rx-7`3i~J3JEtCiubd zKl^WJb@qJb?Z>n|1q#Zk34Gb3?l?SGV)$}!Pqf(qLNk*Eh4@5A{%GE1Hd$&QNXxmq z^z|vTE z!a0fUkIhn$%@NY6_A6DIHq1Fu(!HiQcm&Uq%TnwWvh38+#(Yv!yrqT~Y1SlE{;UDk zS%BLIBufoNJY09wQ5|Z15Q@-{Luc>K%Fm(3lZ1(s24tLo=SGAqlXArV!9Y>1koZY= z@tM;>6O=!xIx#8@dKaJ02L2^IU~q!1L?`jr>kOC=f$R33WYqq#n29=I&k_-IUu1^UYC zUhLOhrsfvx44jLuc3$UKAACfXVHh&41O^%&y}GB6ee!I4qBBSo1&+|Ybo%88a#H?< zF5mogdWL2ak2nwUndNWt2@>#qW%ns%Wa8NqoC`So5#stmr)0F%Dp7qegCPneCK0#m zrVv;7Y-6X|;GmtZo|Q|UuUTa8{c=2klCE~II>RNwa$n9Z**;L!Z~=lNFMC?4q~5at zP+gavqh#dnRgKgEoRrl=<9UP=a+USEDAC;B3lsu^XR1}TgAq~58Uj(R(ZBChCrg8N zs*bK=c>S8IaT8mhlc$YxI+cJJuJ8vhMT(Q$LbGxxxl0UHmozG`70N2QXqLW@c}g2r zr4qSKHmGi(eQa=jZa?-C-D+`@pRzIr90j;zFnMw)$9wiWg5jrZ6jd;IL)s&>I#Mtf zonU2sW}vdEjx37XoTofDyaW$EYCJkBw<5Qt!nZ27DyvTb?rUE5E)Kw!oXqX)YZ3l` zgsT+3 zy^tIBkDM1W1=)vr-G}4^0!jf}>`^l(3a6es{st!osxU`Mg0co)e#dix?5R1$o)7rb z7Ri{uC^TeqiE|&QHDrLLWdN@POx~0^&lnH}rz+y!!ju_z_W7Suc>23*GIIEVO-u15 za0IQ&eU5nghcA%J+y(_^ick#h6HNm=u?8nqMaeV75ZRCF7hOr3JgchNjlt*HT54mS zez;jI)-HsFk=f7l)+f-_j)Kyu+`V$CyoLpnS+&Y&lGn{OM@_mj7zK|tku;a*MF=$n zF70#@(=~%l^BnS8Z98(AdPu5~oe_X-oAPNNtme*}62#~GCY>Eo-NV*J%~zWxnqJ|} zLm?3@?Dc%6aQttm|7onF8X#g67$6`MH2*2;qmC><1aDmpjPE~1)4zRu-Yq-b-DK>veN2$6 z=#1V4O0r>AJZVAwuDGZSbZeT8z>YB+^;Y3CUGUh7N|PKY8aE)@gL}&8j}A9wSGl@j zA%$t|>0r8y#Pt!3G;`tNVuEF~FKGTtvHmGAE*%7k+8yG6Qg2RgJ1oy;KRm$RF&;%F zjTOyg#a&|@&SYv|(W4%WJjZ)!%5PcIL|W1Zgy$o)haZhr9EOu5N4@`(SflD#=xSUbaJfZ8;=Lz#o}+Fvt^GoSL#{; z)nJ&&ss%K;aZ*t*wC7Sr<7e3I#`R_21r%$O(^})QW?N|v1Zs7yhv_ZSg}cnFwqM(- zJf%=QmpB=@wWMHVBbCFP2yi1_Z7L82K981JexSYDbi~M{O&PL5Y;kne!Z&hIm!VSU z&8dBqEYVxy0YeV6b1s4cJVDV0N*e#U?E~d=R}L>ns!Dld`s;Cz3nh)t%f=8xov)jU zFksRhA)0Z|wYx?4H=@gUb^;!pP>;pH;B1Pdo#B6y{4ku(heO)VRFNRXG-k2ki^+1R ziVpL!vH^5Q|tEqN4 z#l3xT7ieBfN=MMJ%wAyHk6Xn66or~8=G!sRjyK)nkK8v3qA`=brb+iPkWzUC zOTsc}IdLc<1L9)Dq@Uu2{i2ioH?Hc$oyS((K8CmC;87HqJsCQ#J3bEbzV0KBch0K5 z2oLiLCWO|4r*@2taT85Akak3SGXsZ;uVx@M;;)3B0wMu#?Jlk?Y(?$WvANW!R$UeL z>OL>%Xd3y~8Cd(0u{c?R*4sOWJNFHVVf6E!@6OAC%e?f#Nla5V4X%y!7+&Gew6r!E>dtWD zg16gd(%)(>{9!05z&m8m%w;b?L+NN#X8v*s$;I-Zi{g6winbvu7}ffjR6{eP$*&ny zh}a_@Y8`3cf;~rm66H3xY1u!h7)r29F^p63j%%t9O(9bCp^m4!Elo1%uesK1!MXDo zBsBS)=N~Toqa>TbxrrM_LyB`_Ol>%8!G;Nu3~sEb6%p%-KnO=iFGXM zvwKr(P8Rf!i2WR9uc>Cofd(CpgI!DSDZ_zDT;J$FQFJHka=;;{FWnxVUNmF`S_6uc zv7;b@`^5NKGe^m5y+yp%_r>aCqBhEO#S)iOkHEq@A|QF@mAF_GFER$OAYmpQqjj7m zwC^gd(3*V=Sk!vkBFS-{Z5u3ifJVjnkE!8jV>jAwoy;c1_hw$gII4oD&7(Zra|9Mnr{$n#$Zr`kUe!Ixh_5#P#1Wh8F!bdp8_0;{bl24p+N z21)ED{p?{MSVZ?EbSXT}8gqk28>fu0=&j`Iz#PRko?@@F?feMQfoimj4E6jt z6FtW0G=j!415Z1Nc-!AWBYxf2%$2WphL5EXfrx=_$_Br>G|`jqT4$SOzNNnI*9_rsV9_+=zTa zYG{q7Kp_Dk7NHW@x4VZi9sgp_1MED3nUctbnRihU2P8nFa9@pLZts#4vh;gRpN1-?G7iz$})ta%J`x4RV!5)QB(1 z-rBJA`w6Ei(3>`sud!@S`>tO^3^#NmFIj8F0IdzPqj)rz_bO>0D*yp90#_$Q`MukOYh8nVF z>K-q%!qs3}B;A|L4@^RwR+O;}=R}!otdDUNHxuXW*?L0A#M{)R6^JA-gA?Um1R#-@ zfow@TKNA+@96*m@vMT}7WN`-- z2bRh2Emd1q$mN^S;aVy|eA0r^RN+hN0(YxKvd}F5Ak6yx{<-~mN=|a2%B%|Qx&jG$ zVZ~9MwjvmY^98zb%!EHw<@ZM|SJdHzxWIRKyUHsg(p$9yP-7pgd2^3h^eeuV8NvcD zBGj?okU*I3J{98QJhAAakI%prJQeC=16lC>mveom4%6yn9Mv@eo^WFJ9K;v67eBdg{;1xqSYXkuG4M=^2C>Bg zgh@IT&c|#(!Z-X0MFsixa_HUPueFYDmk})|CV(~H4W0A1YC+r9;xY?4Wr;YAnS!kR zW(4?Xes^hr4@IEsscW20uZN#wwpbn42Kq)=#Knu=I6y_yz%b+;0 zKtBof{*2zxim1=@O`{>t1bA9Fz(s$qRuCT`oONz;z8U@RFT*wowsT^rz%g9KP&XBI z)P3R|jnql8^JWKS9)EX|nOt#g_|I7(0~*yt_xr?j?2^s1#6--U+Lyl>Sb+(1sL_7G@xb#Pl9G78ijyP zRSG-p+fd~`x?_Bs>_X*mbAka=i=0;@cFF@uz1BkazGpoVGE+n0vsn&@OHgzuIxA9+GSh%{`xftn|Mvwmm2AUoqFIo7A&p z>c+X7PBHcYdexoqV`^Y8zczYLW-&)FKQ&S6+gj)~-D0u3}p~ zBE-WXQ;~ZKlol%V9}o<8lrmp%1$rqK|0>3TG~$A^Q$UDJ4M*B#2M9$Cv8=vvQaj@? z`m=RXCzEh6Us(CIfxNRr(5juBfIZB^_4j`s8%3BnlEEW55Rh+-|L3v6O5AaTPTZ-* z1DeHe{x1R~QunlQr&WL-_E;Q*&+^CEjBAjSc^=+ zthbj=Y#PcWu#x3&T-mmhvnvN4m*iuOFR8UlIa`zx+S;?(;0oPe)vVC3#XOvE zsp40pjg@hmHcO4%=~QuXws7JaM0)+>XNcx`r@JT!OUWkoIsFtBVs9uO?`30E7=NLv zpDtRF`}4}&tBo=Us>ano<->3=C*-iB9EZU&;cIv>Dl_DpSPCXd;d|XXDyj>l-R5wS zT$zXqKU*|Rouuz&P%C_=)m-=Q+n+ED{JuR;7B)6}obV4EXN@T1rZ%>PCoAKnHsP-| z2M3pg>-(X;Qd4Wl-24@Fw!vne>-PhIt&J}D z-^&os;RCh#=YTo+_bAIHHYZ^xHu^CEVLb3gU46wsz}o^46ADaE>sFI2{kf$HP-w(j z@Ch3fPzumhrHsZ?Qu_k*VAjmsuo5g+WjDsGVp^&3&7W)exeP7#l@?ftpe!p5U&< zj4^8-sn}-4sNwq!@vU6}$s_x1LLEW(<=G`cpzjmkR$7#mr9d>CZ*aFp? zh%*B(H8BJPo%=YSE*EG&Py>>HD3wN~kMGf0jF#DEv92-e0LoJLociW)nC)wPW&*9r zqmM(3rt=~~TNVdr$!5R`l}(k>#Q1gqD9%MxdSg>mUUW0~ z`{W|Berc}432clb;Cwkz@gkC$0arF4{ z8M|cf8s*{F3+CI7>L4Hyio?BY)bZiN5^-`f7Wyt|GZx$mqFQxKDLTLnNaXciu9LsK zgk40;;6O}N4WsIZ`*P7jOw$zAyV9SXq1Wtru~Ho{Vr@`&IkzoyCO>$7I5;72wtj1>5m69#t|HLGbb&$d><^omVmuu9`6V%P6P-wpk6n zfG6tKm^+_k%h&d+J8(%P+nexmnZH#o*3QQ1w~(EdB~+(F`rZQ&k29(6a-8^z-fA79 zx*48$8JK4#lHQ{RQFRtXxwVkjlFO*ZDh{ril>ywn^WzZYX(^Iho** zv!uM{A2Kol`s zk{yGRH3KgDGYuDe_iAQ=+~y8@Xpv`fKJWo7iwNHfbRVaji4$Dz5%TQn0WsyXKVn6L z>zV<(9o{0?oz^ACe}S8d#czp}ZKTtPchyT-NEZNXgsyM=@ow)GD=H*^bEF>A(NBQ1 z5IgrHW{+qXS!{UdlQZZYvzC!AKw(EoXr{wkSlQbOiJ|^Ztm}@Z2xYb6w1ZbqZ8{f*&l zoMmkc$=2F-oF0y?u`f5^XRRSYmV|v}I1jQj&gUy9#ie@v2M!yWG@Vz;fzm10<0uOW z+-*)zkAr6I5?{70(?nFmmM7FQs4=&ri=_s-Uj+=(pNw_X1~RP}`lJuKG3K}MU^Cnl zQdTE~%MLcO>(Hp6B>*<2a+=^m)e$O>!pTBR+Roc96uHA^Si#(waZ==V+JCkt!jy}1 zsy?p6JjEmj*TQEh+X>!23AdP5t3W0Jei;!eC>$iT**9CtySnJ}N^t0fR{Ly#fQSGc zg`y*z5p4z`7CG*Ctyz2mR$tF7u!^2N&n#v0Pfk6Pa42r`#mhv#`tA&rj z-XY$=2k#O^-aOwRAcnU}g;5~xS-_^8rKYF2M^*1>DXLDr$I_@Zw6xGWCbo`N6;d0s z^5(jJTGi@|U9>zfx}=rhqyQavd^Lciz8xW91NWxAW*5Jmy$Cx=L4+oYCoN8SPe+z! zJC4GGN%%8~fWf}W!$1U2v-p+Y`Nu`MzF@dyze3}#10GPJE>BL)kS7x$*h8O4bPp_I zG-+wlK!9+|C52{YyY~hG*KavxFtT5qJ_h+(r|vJNOCFD}YGprut^Q$PnM?%=hhk7Y zU^eK)JcB{B(lUMYzLfKs7K!qml3;$hX)6E2n{2uy zPfpFMw$0|ww;~UiB~qI9D_JZJx#^Q z2S(F2%+eB{wiU#ITKj#ayy5`=?jC6)&i#Qyg{}t<>8E_LQ;pg`xYg{V9e?4 z$RsqRt&`ur;^*fl2D9O55rWp_*j|A1)M)b&*$#e(5f)uyVFXb%90dnXwE6u@SNqgZ z$4B;E;%)WNc#{A#n)MK{#S5@UVwyAe<)Tgr&m%>z_ZAt@Z(yyJZTm@)MXsqqXmTX* zSpak1xw5xsx-21=D1+YyXreCAsY>sqKf`MMQ6R05bvCs?h7i7bpU<0!rvn(D~%l@=BWb zC`=44f2MctO|f`}ME<6dgUDHn21QDIp85?#+T;ScPY6O(%s$w|-V`?_S9p7vDG^J0 zjn=Jg#I5JK;bsiFETa4nNW;>&D)r%jfublnL(qs)_3^blEdiX;!A8k^f2`6RYft{_ zhnRHy@>?5q!_NYKuO@$ANcz?j@zknv?-3khX7xDgsi(Sr_V`>zgYD;$h79TUnKl;B z<4mhWx9hrt4~2==F5b_DBV(W=OZA(!c()NW1dtB5`y>kU%xO;}G_b>k9SG~U@5-0i zk1tzu6BF+3cS@ca5X{Fqi62xSkMmvqh{r^4=TNuMX-Wdx1R-`yU?y?np^{-XSVAN^ zlhgM7vx(AyvFfF>%lnkY5iMVdbz#_vY2FTQ5?UYjD(+Rre{)i75;I6$K*4Ly$^JT) zd1w*diFETJclFN5sF^=s+GTGRuUXhQ)qowYS$La76+~$A=S;y2hpHvgY&9r6s@Z&B2T98NWk+3R=E1n;^t-8XnF5C~1D$-*3~zl#Cv4wIL)yYMiC&Mxhq@(`C=A-zH6f@L=6UG&oI&cJ?RM%n%pH@!TFi&w$HMo%Ia=HKi*lkTN_ zMdcUJ$~q#{J16ChR6r2IW-%0Se!*D3-iwLk8R=3ZEfn;4Qtz9Q^WgFd@cw%-t18ts z+*%lKpOcyYT#TyyVbXTf5a+(ULh5r%-7-2C8k_UnPhuGHR}f28OyX@EPMIcyc7cNX zeZ43*I)OLm){B&E=QNCoQMcF4)ifV?qFOgQyB!DE@`C_a5?l>Ua}o*q}+EC5|uN&^; zkp-ahcePTDs~1NpIr{ni@rV1sItoFMLsnIQGbk@pAi0@W%h`HEYofno05bh4NJS>&zER?b&t7s&{% z(RDY*LESctrPnknaDEN`bNXmWncOv4G^*(|yu?9q@y`#EKIKKBT`JA5FRBj^S&6sR z2Fnle#a_Aj@AT|;|3>Jqa;V>Ix;{`Sq)Js_!s+cp$=c);VxG(Z^qht8J~tI$UjmUm z=3{YnQ6(b*0}(pkJUuA4m1Grl#7G3rQf|hfY>E1Ioh{b_>YrLeD+L~jA6JYK;sdMLA@|Q%!`=EjA=koUt*Cdy zwQe z+@Wg1$U|1BC0Ow9M)?^;K*HTmH9Nr6xN>peU(t4GoF?q{al3P@F+)p+_weT9gbpJO zQKT}4bsaxpFZ%-hsWWLooeBB8a4x%@<5KIljaJS7GSkC~B1cc}g9{!2?;;APN!1!$ zOaW2bWjn7Fu*_St!CfB^wOyF)A|$wli9gk`8+^cfrWbYAPg343&K&D;eC)aMzTLbO zpW2VU?jK=L|Z z4}xLeLz;0s%%g#UY|uKfnTW!m$l($w%T<8UZGx9`i5)XBuV+}RZKI@(r#btwJ><%R z++3`(NAlWofEFC%;QSCb5Z29YpzqTK-~hOk0SSYVrMUvgn@)od68gN6H%bU5Qsj<9 zmECyJGama|XqK4CncJz5iqB;Ka2j$vk411qLCfb#<+AOynl4fx6oL*yg^K~}{nk*h zJuhdF* zd%6$c++?+#6A|r^Xjbk1b8G_yJ=Pd2mS7ao>T!oyfo#y#1w)|I67!)aqMi)Zd)Ytk zn{xg>c_XK;1fSGfRuBvZUFRk0NDW^tx~(2i*~_p-#y;RGjhK4=nfA?=w!5=CPhaB* zws7<;#G>=!UtX)Mgy<>%%pROWsrwaEZc@~{cYFt9p9_=vtCWiVF{oX<=2sV8*K^HB za3E;qR_rYnz3SR0VM`Q=y>L&Y0=2F2U-02cqjPk6geSTzqa)UlR6muY%(wq~MY|+-`=@dmpw7VMKDWM>RVuH?1vhi6%MjpcLH%s+|W_ z#pMFelu2|80s`baTs>w!CMANy0mMWF0s~~VvzfYU|8AZ8J({wFwu8X?%^Mq}L9Nh5 z08%z$lQCVW$U#I82$^L#eCJ0j;H|H{3KdVga{IH@v>oPyJc0{H4u!tG_U@|aT!+TbeviFJo@HJpIfOWq_?$I z!QhrEGa~Q%?LTusl4~?Zm>;R;)#Fp^sSbuSt9SZddf>t zTYhtNlkv1Yb-l&?p0l;`2sLE#|K+X#(TeDunaq-c>yI_)3~al`7?j9O?EfL8?I+Z+ zU+e2pOdep@C#HIU_0~G%w-fvvqEg1G^19Gg8x#pOaAzv?2^JwtNEWIP^@Q&{;3IiV zyc$+vx+N6Y`!V#zatT=U@jG!_uEFwu;t{*O)jTH5Gd7Eoa6#b-D!sEy8SGPm3-_-Y z#xJ-5@D_3dhFcHbDpnq>dQlb^D>DtngOI@H{jN*DMh%F)gv>F||P+*x$=d z3g2D=m)p=Yg<4WbjLZ-07ThMlP7UCEYJe7dS8+izy0*KIt`rd?D>!{97WNERRK~|e zODeLO@f(gJZpuM8^p1xAJG1awv|eeuR*5S6kSlyeQGm@)XO)-gqlB;t0#Q}}66_76 za`vKfjS?pEXM*ux*Ks#fIawf*%(x@(ZBuie>a}~Yfs(e_XwRR}g5tk{*d}Aj&73f0 z!|!DZ=p<8)^3jrl3R;90!nXR%6O%c0*8=SY&A{SGaLi)?HpSzrOr?U$H4Tat$MZq(^=wj_W-K8~)|=>DxMhPc1CaNd2)ncn`f z*n}oof;B9M$JQ^vCg+Bb^ijTAwTEq#8e>i_!m_TbK|pALO028L2l^!1 z{X^j)k6PrgH(*Cs8GX4>tw)$Hm!ob`$E|WOUMzT4Ezj%(#1&Q-l0L^7&x^GAN8J{wf2Ea@mR+Y#? zlLJ&U(k&%%aOf;fz@NC5Fk3Uj12xOz={0oDrYx7>9ot1Q0;j`WX>92>8`oWE z8iN1=C%4HV1-n5i#pCRV>9pgtCLL9>+`ngBz*Yxdw>}2E8FkE0_N9oBLB9f4=HxEh ztC&Gm6jD_a6enTj^iP`%EWFOM&A2$4N#zi_n=P0;puv^Scob;VlU08Y1JyKHyXTaI zsV76qCx;5k~ zYFyias8X2u+CCsj7*J$O5jlkL1MGpyJd^oi=!Dp6RLt@f)F<2l`SoY$3CU526#hp( zo#5H7mAm&sKU>X9K_FB6K_p_1>u@RlM?9b4(Jqgc_eMWa=fNUk4?G>?%ajFQ7pzjp zEwtNc|LSfz9HHy2McsACX7upy@IQp7pZ|rzt4_2@-++REfhhmqZ-OI-3;6DiCV~DH zB8V#L!Oq|?haKEvwQw5;fGunC(|jwSF^cfpG)-XK{uj2OpeuY^o%iA!@FFgPj)C5S zA?|yN?|U12=V8HRmB9JFK#Y#PbIw2$C z0g+(OaWUAMHF=(6f!)lWnf|Q7wV|X+{H^4;m6@K}l1CAXJEH*;#blu@#>QGzk%irD znoudUI&s+!5yG;vYT~#M{t==0X<~G~EjGU(-!T^pu5Jtr%Np7*2HoCTW}N{#}RbyDvOnBu>K51gd>X zLWlH!$~yOWsL}?GAJgPgOj=Bf6S?0nFCt_tL$1Tx9d{BUlS_@Z&|q?VZNsn}rkj*P zUe}pyB=Ndsx*$SXYHMm&O-xuNtBXBOKEr9}^O=9<`Ofb-&vP!H^E~G{zn@9pqn4sE zJLBMq8a>IQQq16uOP4FFX7NT3_tgO-GCOYd6I@;?x>dDMU^2&dkvU7 zT|F2dTwIhq*>oUDTkjjKWe2Q4;|HpWyJMbpH=W`tIdvCbw(EJ2yRL;A?Sr*WrTD|7 z!Q6L}o2B-K6_iz$kpo9!)s*nwV-6CUjG_s?WXqg5F}8Yy;Wr!Bc=3O@Sjl-T3HQS3 zWcGj0s0=9lq%$ALAsKrHHatAdFASy1538neIl-9^2nK0{JK(W!ZXdQMeOqe@w)8Z= zpYA{NQ!T+UHK%S*c7@crfJ?vqsu(PJukuW3z`ILa&eMDBze}~{OR^xFbSO1GqVM&= z4vC{i=ZnJQHC_9&tVbt2+cH(bB2$l{Fc0?(vshV@?Tcqy@VxDR;XHZUJ=Dc)&9V|I zWpJggWa7BZV{rIwfrw@QwOWeukcZI^-{R?3Cd3HhmD6L4tp)OeIfZadXKgQvcNC^L z)X$Ff-i1-}i#tJ3lvtKCA0kG4ChABWW7HZGTz7^0lw2n5?WK}C@(YfYVL$ngmPZ}S z<0kb|eD0fOTK!sEI9qd;zm3^@VJhykz1{-+O1_{WBz(l8m3p@f?cL(OG!?RpEh$4bn}NBXooR}278UFWGj-+hEk~a z`D9anC^ue5&=r`aRCvfGufN%5S4Kir4XafnC^7#wK6i*<*pZ_@;X~-iR=Sy=|5rhA z_4Qc^qIb|x59^bAkokq@`a4@3DBkoZizT74Ek0hKlT%cJQtzMN;N0 z%|%>rcF%p?aV3i?w=tDJA|oX-a6GK04SQCsxILrPhg@xV+~wW=VPcT7^u*RFe(sG| zX})O+)VX0_=JkbG7f(!~p>%RU2|@JqbZxU!it-DjCW)+#}uPX zt+{|G%#}Ammq##B+=!beac{@3ZV`)35?5;?AMRsusdrgd_#w{H-f8sRI^tV3K6o)L)dSNXaULi9-sPLXlUf>R zbiKP!{c{Q4=S}@LJnjwMI=JF8~SY2FRRY`wJD~5`(uy(r>q&9&K)4Y$gwzCJ{}tiq1oT~{Js4dpb|w%m zZ-Q_7Oi|@-GKkO#ycpCiqX>&_RTM^?1Hf(+D~(@e^;Rkc*XO|6pg@kiP_*>QPEZ*3 z3D=HR6j2d^%Bt#!>RL`B0H9Sn)r9cxaPB(13D}irKB&D4kCD<^S0)NASLuSnAU3#9 z&}~-ILBY1Va!Acc=oh?>V*u_b6I6Rxdcr^4(pOl4HzJ8mkVv&0rq%!{95ggURMuin zzTOoq<5d+=IL%2JTnjDu8Z6%lO;y`%M7;K)|J2A~Y^>lPRH1`48lcc#{9k)=-WsZ@ z@l+I=Tf@&GQH|JZ3zsWG#ZI%-NcA%6bl3@UZ?AXenaPkW7I%O2(pMz|dY2laS5}C1qkIRGb z7kzS1F%j@3TOa%~rdt`(0e~S&EhZm<95`m+>P;?ukNb6>aUlfyG78iO*I$;jQECdO ORzXA2f%7X@nSTIfr%7r6 diff --git a/manager/gradlew b/manager/gradlew index c27469b1..adff685a 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -115,6 +115,7 @@ case "$( uname )" in #( esac + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 90a3f3c5..c4bdd3ab 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -71,6 +71,7 @@ goto fail @rem Setup the command line + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* diff --git a/manager/sign.example.properties b/manager/sign.example.properties index bc70a60c..7e73c02d 100644 --- a/manager/sign.example.properties +++ b/manager/sign.example.properties @@ -1,4 +1,4 @@ KEYSTORE_FILE= KEYSTORE_PASSWORD= KEY_ALIAS= -KEY_PASSWORD= +KEY_PASSWORD= \ No newline at end of file diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 1bd53bcd..70f591a6 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -189,7 +189,7 @@ enum Debug { /// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled. SetManager { /// manager package name - #[arg(default_value_t = String::from("me.weishu.kernelsu"))] + #[arg(default_value_t = String::from("com.sukisu.ultra"))] apk: String, },