manager: Restructure the file directory to keep it clean
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
// 自定义背景加载逻辑
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
package com.sukisu.ultra.ui.susfs.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.*
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user