manager: Restructure the file directory to keep it clean

This commit is contained in:
ShirkNeko
2025-10-12 18:58:02 +08:00
parent cb7abc88dd
commit 85291de02a
37 changed files with 2705 additions and 2265 deletions

View File

@@ -1,7 +1,6 @@
package com.sukisu.ultra.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
@@ -9,47 +8,27 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.activity.component.BottomBar
import com.sukisu.ultra.ui.activity.util.*
import com.sukisu.ultra.ui.component.InstallConfirmationDialog
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.screen.FlashIt
import com.sukisu.ultra.ui.component.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import zako.zako.zako.zakoui.activity.component.BottomBar
import zako.zako.zako.zakoui.activity.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.util.rootAvailable
class MainActivity : ComponentActivity() {
private lateinit var superUserViewModel: SuperUserViewModel
private lateinit var homeViewModel: HomeViewModel
internal val settingsStateFlow = MutableStateFlow(SettingsState())
data class SettingsState(
@@ -57,12 +36,7 @@ class MainActivity : ComponentActivity() {
val showKpmInfo: Boolean = false
)
private var showConfirmationDialog = mutableStateOf(false)
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
private lateinit var themeChangeObserver: ThemeChangeContentObserver
// 添加标记避免重复初始化
// 标记避免重复初始化
private var isInitialized = false
override fun attachBaseContext(newBase: Context) {
@@ -72,11 +46,8 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
try {
// 确保应用正确的语言设
LocaleUtils.applyLanguageSetting(this)
// 应用自定义 DPI
DisplayUtils.applyCustomDpi(this)
// 应用主题配
ThemeUtils.applyFullThemeConfiguration(this)
// Enable edge to edge
enableEdgeToEdge()
@@ -89,146 +60,16 @@ class MainActivity : ComponentActivity() {
// 使用标记控制初始化流程
if (!isInitialized) {
initializeViewModels()
initializeData()
lifecycleScope.launch {
ActivityInitializer.initialize(this@MainActivity, settingsStateFlow)
}
ThemeUtils.registerThemeChangeObserver(this)
isInitialized = true
}
// Check if launched with a ZIP file
val zipUri: ArrayList<Uri>? = 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) }
}
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)
}
}
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
navigateToFlashScreen(confirmedFiles, navigator)
},
onDismiss = {
showConfirmationDialog.value = false
pendingZipFiles.value = emptyList()
finish()
}
)
LaunchedEffect(zipUri) {
if (!zipUri.isNullOrEmpty()) {
// 检测 ZIP 文件类型并显示确认对话框
detectZipTypeAndShowConfirmation(zipUri)
}
}
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<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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))
}
}
}
)
}
}
MainScreenContent()
}
}
} catch (e: Exception) {
@@ -236,96 +77,69 @@ class MainActivity : ComponentActivity() {
}
}
private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList<Uri>) {
try {
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(this, zipUris)
@Composable
private fun MainScreenContent() {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val navigator = navController.rememberDestinationsNavigator()
withContext(Dispatchers.Main) {
if (zipFileInfos.isNotEmpty()) {
pendingZipFiles.value = zipFileInfos
showConfirmationDialog.value = true
} else {
finish()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
// 处理ZIP文件
var zipUri by remember { mutableStateOf<ArrayList<Uri>?>(null) }
// 在 LaunchedEffect 中处理 ZIP 文件
LaunchedEffect(Unit) {
zipUri = ZipFileManager.handleZipFiles(intent)
}
InstallConfirmationDialog(
show = ZipFileManager.showConfirmationDialog.value,
zipFiles = ZipFileManager.pendingZipFiles.value,
onConfirm = { confirmedFiles ->
ZipFileManager.navigateToFlashScreen(
this@MainActivity,
confirmedFiles,
navigator,
lifecycleScope
)
ZipFileManager.clearZipFileState()
},
onDismiss = {
ZipFileManager.clearZipFileState()
finish()
}
e.printStackTrace()
)
LaunchedEffect(zipUri) {
zipUri?.let { uris ->
ZipFileManager.detectZipTypeAndShowConfirmation(this@MainActivity, uris)
}
}
}
private fun navigateToFlashScreen(
zipFiles: List<ZipFileInfo>,
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
) {
lifecycleScope.launch {
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
val showBottomBar = NavigationUtils.shouldShowBottomBar(currentDestination?.route)
when {
// 内核文件
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
if (kernelUris.size == 1 && rootAvailable()) {
navigator.navigate(
InstallScreenDestination(
preselectedKernelUri = kernelUris.first().toString()
)
)
}
setAutoExitAfterFlash()
}
// 模块文件
moduleUris.isNotEmpty() -> {
navigator.navigate(
FlashScreenDestination(
FlashIt.FlashModules(ArrayList(moduleUris))
)
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState
) {
Scaffold(
bottomBar = {
AnimatedBottomBar.AnimatedBottomBarWrapper(
showBottomBar = showBottomBar,
content = { BottomBar(navController) }
)
setAutoExitAfterFlash()
}
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = NavigationUtils.createNavHostAnimations()
)
}
}
}
private fun setAutoExitAfterFlash() {
val sharedPref = getSharedPreferences("kernel_flash_prefs", MODE_PRIVATE)
sharedPref.edit {
putBoolean("auto_exit_after_flash", true)
}
}
private fun initializeViewModels() {
superUserViewModel = SuperUserViewModel()
homeViewModel = HomeViewModel()
// 设置主题变化监听器
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
}
private fun initializeData() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
} catch (e: Exception) {
e.printStackTrace()
}
}
// 数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
val isManager = Natives.becomeManager(packageName)
if (isManager) {
install()
}
}
override fun onResume() {
try {
super.onResume()
@@ -343,12 +157,8 @@ class MainActivity : ComponentActivity() {
private fun refreshData() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
DataRefreshUtils.refreshData(lifecycleScope)
} catch (e: Exception) {
e.printStackTrace()
}
ViewModelManager.refreshViewModelData()
DataRefreshUtils.refreshData(lifecycleScope)
}
}
@@ -363,7 +173,7 @@ class MainActivity : ComponentActivity() {
override fun onDestroy() {
try {
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
ThemeUtils.unregisterThemeChangeObserver(this)
super.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
@@ -378,4 +188,4 @@ class MainActivity : ComponentActivity() {
e.printStackTrace()
}
}
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.activity.component
package com.sukisu.ultra.ui.activity.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
@@ -17,12 +17,11 @@ import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
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 zako.zako.zako.zakoui.activity.util.AppData
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse
@SuppressLint("ContextCastToActivity")
@OptIn(ExperimentalMaterial3Api::class)
@@ -40,9 +39,9 @@ fun BottomBar(navController: NavHostController) {
val showKpmInfo = settings.showKpmInfo
// 收集计数数据
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState()
val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState()
val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState()
NavigationBar(

View File

@@ -0,0 +1,429 @@
package com.sukisu.ultra.ui.activity.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import androidx.compose.animation.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.edit
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.MainActivity
import com.sukisu.ultra.ui.component.ZipFileDetector
import com.sukisu.ultra.ui.component.ZipFileInfo
import com.sukisu.ultra.ui.component.ZipType
import com.sukisu.ultra.ui.screen.FlashIt
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform
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 kotlinx.coroutines.withContext
import java.util.*
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 AppData {
object DataRefreshManager {
// 私有状态流
private val _superuserCount = MutableStateFlow(0)
private val _moduleCount = MutableStateFlow(0)
private val _kpmModuleCount = MutableStateFlow(0)
// 公开的只读状态流
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
/**
* 刷新所有数据计数
*/
fun refreshData() {
_superuserCount.value = getSuperuserCountUse()
_moduleCount.value = getModuleCountUse()
_kpmModuleCount.value = getKpmModuleCountUse()
}
}
/**
* 获取超级用户应用计数
*/
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(packageName: String): Boolean {
val isManager = Natives.becomeManager(packageName)
return isManager && !Natives.requireNewKernel() && rootAvailable()
}
}
/**
* ZIP文件处理工具类
*/
object ZipFileManager {
val showConfirmationDialog = mutableStateOf(false)
val pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
/**
* 处理传入的ZIP文件URI
*/
fun handleZipFiles(intent: Intent?): ArrayList<Uri>? {
return 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) }
}
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)
}
}
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"))
}
}
}
}
/**
* 检测ZIP文件类型并显示确认对话框
*/
suspend fun detectZipTypeAndShowConfirmation(context: Context, zipUris: ArrayList<Uri>) {
try {
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(context, zipUris)
withContext(Dispatchers.Main) {
if (zipFileInfos.isNotEmpty()) {
pendingZipFiles.value = zipFileInfos
showConfirmationDialog.value = true
} else {
(context as MainActivity).finish()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
(context as MainActivity).finish()
}
e.printStackTrace()
}
}
/**
* 导航到内核刷写界面
*/
fun navigateToFlashScreen(
context: Context,
zipFiles: List<ZipFileInfo>,
navigator: DestinationsNavigator,
scope: LifecycleCoroutineScope
) {
scope.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(context)
}
// 模块文件
moduleUris.isNotEmpty() -> {
navigator.navigate(
FlashScreenDestination(
FlashIt.FlashModules(ArrayList(moduleUris))
)
)
setAutoExitAfterFlash(context)
}
}
}
}
/**
* 设置内核刷写后自动退出
*/
private fun setAutoExitAfterFlash(context: Context) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit {
putBoolean("auto_exit_after_flash", true)
}
}
/**
* 清理ZIP文件状态
*/
fun clearZipFileState() {
showConfirmationDialog.value = false
pendingZipFiles.value = emptyList()
}
}
/**
* ViewModel管理工具类
*/
object ViewModelManager {
lateinit var superUserViewModel: SuperUserViewModel
lateinit var homeViewModel: HomeViewModel
/**
* 初始化ViewModel
*/
fun initializeViewModels() {
superUserViewModel = SuperUserViewModel()
homeViewModel = HomeViewModel()
}
/**
* 刷新ViewModel数据
*/
suspend fun refreshViewModelData() {
try {
superUserViewModel.fetchAppList()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* 数据刷新工具类
*/
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<MainActivity.SettingsState>
) {
scope.launch(Dispatchers.IO) {
while (isActive) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
delay(1000)
}
}
}
fun refreshData(scope: LifecycleCoroutineScope) {
scope.launch {
AppData.DataRefreshManager.refreshData()
}
}
}
/**
* Activity初始化工具类
*/
object ActivityInitializer {
/**
* 初始化Activity的所有组件
*/
suspend fun initialize(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
// 初始化ViewModel
ViewModelManager.initializeViewModels()
// 初始化数据
initializeData(activity, settingsStateFlow)
// 初始化平台
initPlatform()
}
private suspend fun initializeData(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
// 获取应用列表
ViewModelManager.refreshViewModelData()
// 启动数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(activity.lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(activity.lifecycleScope, activity, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(activity, settingsStateFlow)
// 安装管理器
val isManager = Natives.becomeManager(activity.packageName)
if (isManager) {
install()
}
}
}
/**
* 显示设置工具类
*/
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()
}
}
}
}
/**
* 语言本地化工具类
*/
object LocaleUtils {
@SuppressLint("ObsoleteSdkInt")
fun applyLanguageSetting(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
fun applyLocale(context: Context): Context {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var newContext = context
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
newContext = context.createConfigurationContext(config)
}
return newContext
}
}

View File

@@ -0,0 +1,77 @@
package com.sukisu.ultra.ui.activity.util
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.navigation.NavBackStackEntry
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.sukisu.ultra.ui.screen.BottomBarDestination
object NavigationUtils {
/**
* 获取底部导航栏路由集合
*/
fun getBottomBarRoutes(): Set<String> {
return BottomBarDestination.entries.map { it.direction.route }.toSet()
}
/**
* 判断是否应该显示底部导航栏
*/
fun shouldShowBottomBar(currentRoute: String?): Boolean {
return when (currentRoute) {
ExecuteModuleActionScreenDestination.route -> false
else -> true
}
}
/**
* 创建导航动画样式
*/
fun createNavHostAnimations(): NavHostAnimatedDestinationStyle {
val bottomBarRoutes = getBottomBarRoutes()
return object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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<NavBackStackEntry>.() -> 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))
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.activity.util
package com.sukisu.ultra.ui.activity.util
import android.content.Context
import android.database.ContentObserver
@@ -19,8 +19,16 @@ class ThemeChangeContentObserver(
}
}
/**
* 主题管理工具类
*/
object ThemeUtils {
private var themeChangeObserver: ThemeChangeContentObserver? = null
/**
* 初始化主题设置
*/
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
@@ -45,6 +53,9 @@ object ThemeUtils {
CardConfig.load(activity.applicationContext)
}
/**
* 注册主题变化观察者
*/
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
activity.runOnUiThread {
@@ -61,13 +72,23 @@ object ThemeUtils {
contentObserver
)
themeChangeObserver = contentObserver
return contentObserver
}
fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) {
activity.contentResolver.unregisterContentObserver(observer)
/**
* 注销主题变化观察者
*/
fun unregisterThemeChangeObserver(activity: MainActivity) {
themeChangeObserver?.let { observer ->
activity.contentResolver.unregisterContentObserver(observer)
}
themeChangeObserver = null
}
/**
* Activity暂停时的主题处理
*/
fun onActivityPause(activity: MainActivity) {
CardConfig.save(activity.applicationContext)
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
@@ -76,21 +97,39 @@ object ThemeUtils {
ThemeConfig.preventBackgroundRefresh = true
}
/**
* Activity恢复时的主题处理
*/
fun onActivityResume() {
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
loadCustomBackground()
}
}
/**
* 应用完整的主题配置到Activity
*/
fun applyFullThemeConfiguration(activity: MainActivity) {
// 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(activity)
// 应用自定义 DPI
DisplayUtils.applyCustomDpi(activity)
}
private fun loadThemeMode() {
// 主题模式加载逻辑
}
private fun loadThemeColors() {
// 主题颜色加载逻辑
}
private fun loadDynamicColorState() {
// 动态颜色状态加载逻辑
}
private fun loadCustomBackground() {
// 自定义背景加载逻辑
}
}

View File

@@ -52,6 +52,8 @@ 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
/**
* @author ShirkNeko

View File

@@ -50,7 +50,7 @@ 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.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.module.LatestVersionInfo

View File

@@ -48,7 +48,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog
import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig

View File

@@ -75,6 +75,10 @@ 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

View File

@@ -53,7 +53,7 @@ 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.ModuleModify
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.viewmodel.SuperUserViewModel

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.screen
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.*
@@ -25,11 +26,22 @@ 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.susfs.component.AddAppPathDialog
import com.sukisu.ultra.ui.susfs.component.AddKstatStaticallyDialog
import com.sukisu.ultra.ui.susfs.component.AddPathDialog
import com.sukisu.ultra.ui.susfs.component.AddTryUmountDialog
import com.sukisu.ultra.ui.susfs.component.ConfirmDialog
import com.sukisu.ultra.ui.susfs.component.EnabledFeaturesContent
import com.sukisu.ultra.ui.susfs.component.KstatConfigContent
import com.sukisu.ultra.ui.susfs.component.PathSettingsContent
import com.sukisu.ultra.ui.susfs.component.SusLoopPathsContent
import com.sukisu.ultra.ui.susfs.component.SusMountsContent
import com.sukisu.ultra.ui.susfs.component.SusPathsContent
import com.sukisu.ultra.ui.susfs.component.TryUmountContent
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion159
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.util.isAbDevice
import kotlinx.coroutines.launch
import java.io.File
@@ -628,8 +640,21 @@ fun SuSFSConfigScreen(
isLoading = true
val success = if (editingKstatConfig != null) {
SuSFSManager.editKstatConfig(
context, editingKstatConfig!!, path, ino, dev, nlink, size, atime, atimeNsec,
mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize
context,
editingKstatConfig!!,
path,
ino,
dev,
nlink,
size,
atime,
atimeNsec,
mtime,
mtimeNsec,
ctime,
ctimeNsec,
blocks,
blksize
)
} else {
SuSFSManager.addKstatStatically(
@@ -1251,7 +1276,11 @@ fun SuSFSConfigScreen(
onToggleHideSusMountsForAllProcs = { hideForAll ->
coroutineScope.launch {
isLoading = true
if (SuSFSManager.setHideSusMountsForAllProcs(context, hideForAll)) {
if (SuSFSManager.setHideSusMountsForAllProcs(
context,
hideForAll
)
) {
hideSusMountsForAllProcs = hideForAll
}
isLoading = false
@@ -1282,7 +1311,8 @@ fun SuSFSConfigScreen(
onToggleUmountForZygoteIsoService = { enabled ->
coroutineScope.launch {
isLoading = true
val success = SuSFSManager.setUmountForZygoteIsoService(context, enabled)
val success =
SuSFSManager.setUmountForZygoteIsoService(context, enabled)
if (success) {
umountForZygoteIsoService = enabled
}
@@ -1393,7 +1423,7 @@ private fun BasicSettingsContent(
isLoading: Boolean,
onAutoStartToggle: (Boolean) -> Unit,
onShowSlotInfo: () -> Unit,
context: android.content.Context,
context: Context,
onShowBackupDialog: () -> Unit,
onShowRestoreDialog: () -> Unit,
enableHideBl: Boolean,

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package com.sukisu.ultra.ui.susfs.component
import android.annotation.SuppressLint
import android.content.pm.PackageInfo
@@ -29,7 +29,7 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import kotlinx.coroutines.launch

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package com.sukisu.ultra.ui.susfs.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
@@ -18,8 +18,8 @@ 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.util.SuSFSManager
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
/**

View File

@@ -1,10 +1,11 @@
package com.sukisu.ultra.ui.util
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
@@ -19,9 +20,13 @@ 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.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.*
@@ -112,7 +117,7 @@ object SuSFSManager {
configurationsJson.keys().forEach { key ->
val value = configurationsJson.get(key)
configurations[key] = when (value) {
is org.json.JSONArray -> {
is JSONArray -> {
val set = mutableSetOf<String>()
for (i in 0 until value.length()) {
set.add(value.getString(i))
@@ -304,7 +309,7 @@ object SuSFSManager {
fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) {
getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) }
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}
@@ -552,7 +557,7 @@ object SuSFSManager {
// 获取设备信息
private fun getDeviceInfo(): String {
return try {
"${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.Build.VERSION.RELEASE})"
"${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})"
} catch (_: Exception) {
"Unknown Device"
}
@@ -1359,7 +1364,7 @@ object SuSFSManager {
if (success) {
saveAndroidDataPath(context, path)
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}
@@ -1373,7 +1378,7 @@ object SuSFSManager {
if (success) {
saveSdcardPath(context, path)
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.susfs.util
import android.annotation.SuppressLint

View File

@@ -34,8 +34,8 @@ import androidx.core.content.edit
import androidx.core.net.toUri
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package com.sukisu.ultra.ui.theme.component
import android.net.Uri
import androidx.compose.animation.core.animateFloatAsState
@@ -32,9 +32,10 @@ import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
import 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
@@ -67,9 +68,9 @@ fun ImageEditorDialog(
)
val updateTransformation = remember {
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
val scaleDiff = kotlin.math.abs(newScale - lastScale)
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
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

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.theme.util
import android.content.ContentResolver
import android.content.Context

View File

@@ -1,16 +1,20 @@
package com.sukisu.ultra.ui.util
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
@@ -159,7 +163,8 @@ object ModuleModify {
val moduleDir = "/data/adb/modules"
// 直接从用户选择的文件读取并解压
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir"))
val process = Runtime.getRuntime()
.exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir"))
context.contentResolver.openInputStream(uri)?.use { input ->
input.copyTo(process.outputStream)
@@ -277,7 +282,11 @@ object ModuleModify {
}
} catch (e: Exception) {
Log.e("AllowlistRestore", context.getString(R.string.allowlist_restore_failed, ""), e)
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),
@@ -292,11 +301,11 @@ object ModuleModify {
fun rememberModuleBackupLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
scope: CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
backupModules(context, snackBarHost, uri)
@@ -309,8 +318,8 @@ object ModuleModify {
fun rememberModuleRestoreLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
): androidx.activity.result.ActivityResultLauncher<Intent> {
scope: CoroutineScope = rememberCoroutineScope()
): ActivityResultLauncher<Intent> {
var showRestoreDialog by remember { mutableStateOf(false) }
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
@@ -330,7 +339,7 @@ object ModuleModify {
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
@@ -353,11 +362,11 @@ object ModuleModify {
fun rememberAllowlistBackupLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
scope: CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
backupAllowlist(context, snackBarHost, uri)
@@ -370,10 +379,14 @@ object ModuleModify {
fun rememberAllowlistRestoreLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
): androidx.activity.result.ActivityResultLauncher<Intent> {
scope: CoroutineScope = rememberCoroutineScope()
): ActivityResultLauncher<Intent> {
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
var allowlistRestoreConfirmResult by remember {
mutableStateOf<CompletableDeferred<Boolean>?>(
null
)
}
// 显示允许列表恢复确认对话框
AllowlistRestoreConfirmationDialog(
@@ -391,7 +404,7 @@ object ModuleModify {
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.util.module
import android.content.Context
import android.content.Intent

View File

@@ -1,9 +1,10 @@
package com.sukisu.ultra.ui.util
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

View File

@@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
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.ModuleVerificationManager
import com.sukisu.ultra.ui.util.module.ModuleVerificationManager
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject

View File

@@ -1,20 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import androidx.compose.animation.*
import androidx.compose.runtime.Composable
object AnimatedBottomBar {
@Composable
fun AnimatedBottomBarWrapper(
showBottomBar: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = showBottomBar,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
) {
content()
}
}
}

View File

@@ -1,90 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object AppData {
object DataRefreshManager {
// 私有状态流
private val _superuserCount = MutableStateFlow(0)
private val _moduleCount = MutableStateFlow(0)
private val _kpmModuleCount = MutableStateFlow(0)
// 公开的只读状态流
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
/**
* 刷新所有数据计数
*/
fun refreshData() {
_superuserCount.value = getSuperuserCountUse()
_moduleCount.value = getModuleCountUse()
_kpmModuleCount.value = getKpmModuleCountUse()
}
}
/**
* 获取超级用户应用计数
*/
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(packageName: String): Boolean {
val isManager = Natives.becomeManager(packageName)
return isManager && !Natives.requireNewKernel() && rootAvailable()
}
}

View File

@@ -1,46 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.sukisu.ultra.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
object DataRefreshUtils {
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
scope.launch(Dispatchers.IO) {
while (isActive) {
DataRefreshManager.refreshData()
delay(5000)
}
}
}
fun startSettingsMonitorCoroutine(
scope: LifecycleCoroutineScope,
activity: MainActivity,
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
) {
scope.launch(Dispatchers.IO) {
while (isActive) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
delay(1000)
}
}
}
fun refreshData(scope: LifecycleCoroutineScope) {
scope.launch {
DataRefreshManager.refreshData()
}
}
}

View File

@@ -1,24 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
object DisplayUtils {
fun applyCustomDpi(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try {
val resources = context.resources
val metrics = resources.displayMetrics
metrics.density = customDpi / 160f
@Suppress("DEPRECATION")
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View File

@@ -1,48 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import java.util.*
object LocaleUtils {
@SuppressLint("ObsoleteSdkInt")
fun applyLanguageSetting(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
fun applyLocale(context: Context): Context {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var newContext = context
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
newContext = context.createConfigurationContext(config)
}
return newContext
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.screen
package zako.zako.zako.zakoui.screen.kernelFlash
import android.content.Context
import android.net.Uri
@@ -42,9 +42,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.zakoui.flash.FlashState
import zako.zako.zako.zakoui.flash.HorizonKernelState
import zako.zako.zako.zakoui.flash.HorizonKernelWorker
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.*

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package zako.zako.zako.zakoui.screen.kernelFlash.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.flash
package zako.zako.zako.zakoui.screen.kernelFlash.state
import android.annotation.SuppressLint
import android.app.Activity

View File

@@ -0,0 +1,716 @@
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.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 com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
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.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.state.MoreSettingsState
import kotlin.math.roundToInt
@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt")
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun MoreSettingsScreen(
navigator: DestinationsNavigator
) {
// 顶部滚动行为
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val systemIsDark = isSystemInDarkTheme()
// 创建设置状态管理器
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<String>,
coroutineScope: CoroutineScope
) {
SettingsCard(title = stringResource(R.string.appearance_settings)) {
// 语言设置
SettingItem(
icon = Icons.Default.Language,
title = stringResource(R.string.language_setting),
subtitle = state.supportedLanguages.find { it.first == state.currentLanguage }?.second
?: stringResource(R.string.language_follow_system),
onClick = { state.showLanguageDialog = true }
)
// 主题模式
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
) {
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
)
// SuSFS 开关(仅在支持时显示)
SusFSSettings(state = state, handlers = handlers)
// 动态管理器设置
if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER) {
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 SusFSSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
val suSFS = getSuSFS()
val isSUS_SU = getSuSFSFeatures()
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
SwitchSettingItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.settings_susfs_toggle),
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
checked = state.isSusFSEnabled,
onChange = handlers::handleSusFSChange
)
}
}
@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<String>,
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)
}

View File

@@ -0,0 +1,509 @@
package zako.zako.zako.zakoui.screen.moreSettings
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.compose.runtime.Composable
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.ksuApp
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
import com.topjohnwu.superuser.Shell
import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState
import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon
import java.util.*
/**
* 更多设置处理器
*/
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
}
}
// 初始化 SuSFS 状态
val currentMode = susfsSUS_SU_Mode()
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
if (currentMode != "2" && wasManuallyDisabled) {
susfsSUS_SU_2()
prefs.edit { putBoolean("enable_sus_su", true) }
}
state.isSusFSEnabled = currentMode == "2"
}
/**
* 处理主题模式变更
*/
fun handleThemeModeChange(index: Int) {
state.themeMode = index
val newThemeMode = when (index) {
0 -> null // 跟随系统
1 -> false // 浅色
2 -> true // 深色
else -> null
}
context.saveThemeMode(newThemeMode)
when (index) {
2 -> { // 深色
ThemeConfig.forceDarkMode = true
CardConfig.isUserDarkModeEnabled = true
CardConfig.isUserLightModeEnabled = false
CardConfig.setThemeDefaults(true)
CardConfig.save(context)
}
1 -> { // 浅色
ThemeConfig.forceDarkMode = false
CardConfig.isUserLightModeEnabled = true
CardConfig.isUserDarkModeEnabled = false
CardConfig.setThemeDefaults(false)
CardConfig.save(context)
}
0 -> { // 跟随系统
ThemeConfig.forceDarkMode = null
CardConfig.isUserLightModeEnabled = false
CardConfig.isUserDarkModeEnabled = false
val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
CardConfig.setThemeDefaults(isNightModeActive)
CardConfig.save(context)
}
}
}
/**
* 处理语言设置变更
*/
@SuppressLint("ObsoleteSdkInt")
fun handleLanguageChange(code: String) {
if (state.currentLanguage != code) {
prefs.edit {
putString("app_language", code)
commit()
}
state.currentLanguage = code
Toast.makeText(
context,
context.getString(R.string.language_changed),
Toast.LENGTH_SHORT
).show()
val locale = if (code.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(code)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}
ksuApp.refreshCurrentActivity()
}
}
/**
* 处理主题色变更
*/
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"
})
}
/**
* 处理动态颜色变更
*/
fun handleDynamicColorChange(enabled: Boolean) {
state.useDynamicColor = enabled
context.saveDynamicColorState(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.needsResetOnThemeChange = true
ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
Toast.makeText(
context,
context.getString(R.string.background_removed),
Toast.LENGTH_SHORT
).show()
}
/**
* 处理卡片透明度变更
*/
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()
}
}
}
/**
* 处理SuSFS变更
*/
fun handleSusFSChange(enabled: Boolean) {
if (enabled) {
susfsSUS_SU_2()
prefs.edit { putBoolean("enable_sus_su", true) }
Toast.makeText(
context,
context.getString(R.string.susfs_enabled),
Toast.LENGTH_SHORT
).show()
} else {
susfsSUS_SU_0()
prefs.edit { putBoolean("enable_sus_su", false) }
Toast.makeText(
context,
context.getString(R.string.susfs_disabled),
Toast.LENGTH_SHORT
).show()
}
state.isSusFSEnabled = enabled
}
/**
* 处理动态管理器配置
*/
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
}
}
}

View File

@@ -0,0 +1,201 @@
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
}
)
)
}

View File

@@ -0,0 +1,394 @@
package zako.zako.zako.zakoui.screen.moreSettings.component
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.theme.*
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 }
)
}
// 语言切换对话框
if (state.showLanguageDialog) {
KeyValueChoiceDialog(
title = stringResource(R.string.language_setting),
options = state.supportedLanguages,
selectedCode = state.currentLanguage,
onOptionSelected = { code ->
handlers.handleLanguageChange(code)
},
onDismiss = { state.showLanguageDialog = 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<String>,
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)
}
}
)
}
@Composable
fun KeyValueChoiceDialog(
title: String,
options: List<Pair<String, String>>,
selectedCode: String,
onOptionSelected: (String) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
options.forEach { (code, name) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onOptionSelected(code)
onDismiss()
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedCode == code,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(name)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}
@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))
}
}
)
}

View File

@@ -0,0 +1,149 @@
package zako.zako.zako.zakoui.screen.moreSettings.state
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
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 com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeConfig
import java.util.Locale
/**
* 更多设置状态管理
*/
@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 showThemeModeDialog by mutableStateOf(false)
var showLanguageDialog by mutableStateOf(false)
var showThemeColorDialog by mutableStateOf(false)
var showDpiConfirmDialog by mutableStateOf(false)
var showImageEditor by mutableStateOf(false)
// 动态管理器配置状态
var dynamicSignConfig by mutableStateOf<Natives.DynamicManagerConfig?>(null)
var isDynamicSignEnabled by mutableStateOf(false)
var dynamicSignSize by mutableStateOf("")
var dynamicSignHash by mutableStateOf("")
var showDynamicSignDialog by mutableStateOf(false)
// 获取当前语言设置
var currentLanguage by mutableStateOf(prefs.getString("app_language", "") ?: "")
// 各种设置开关状态
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)
// SuSFS 状态
var isSusFSEnabled by mutableStateOf(true)
// 卡片配置状态
var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha)
var cardDim by mutableFloatStateOf(CardConfig.cardDim)
var isCustomBackgroundEnabled by mutableStateOf(ThemeConfig.customBackgroundUri != null)
// 图片选择状态
var selectedImageUri by mutableStateOf<Uri?>(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
)
// 获取支持的语言列表
val supportedLanguages by lazy {
val languages = mutableListOf<Pair<String, String>>()
languages.add("" to context.getString(R.string.language_follow_system))
val locales = context.resources.configuration.locales
for (i in 0 until locales.size()) {
val locale = locales.get(i)
val code = locale.toLanguageTag()
if (!languages.any { it.first == code }) {
languages.add(code to locale.getDisplayName(locale))
}
}
val commonLocales = listOf(
Locale.forLanguageTag("en"), // 英语
Locale.forLanguageTag("zh-CN"), // 简体中文
Locale.forLanguageTag("zh-HK"), // 繁体中文(香港)
Locale.forLanguageTag("zh-TW"), // 繁体中文(台湾)
Locale.forLanguageTag("ja"), // 日语
Locale.forLanguageTag("fr"), // 法语
Locale.forLanguageTag("de"), // 德语
Locale.forLanguageTag("es"), // 西班牙语
Locale.forLanguageTag("it"), // 意大利语
Locale.forLanguageTag("ru"), // 俄语
Locale.forLanguageTag("pt"), // 葡萄牙语
Locale.forLanguageTag("ko"), // 韩语
Locale.forLanguageTag("vi") // 越南语
)
for (locale in commonLocales) {
val code = locale.toLanguageTag()
if (!languages.any { it.first == code }) {
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
try {
val testContext = context.createConfigurationContext(config)
testContext.getString(R.string.language_follow_system)
languages.add(code to locale.getDisplayName(locale))
} catch (_: Exception) {
}
}
}
languages
}
}

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package zako.zako.zako.zakoui.screen.moreSettings.util
import android.app.Activity
import android.content.ComponentName