manager: Restructure the file directory to keep it clean
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package com.sukisu.ultra.ui
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -9,47 +8,27 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
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.spec.NavHostGraphSpec
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.ui.activity.component.BottomBar
|
||||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
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.theme.KernelSUTheme
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
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.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
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() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var superUserViewModel: SuperUserViewModel
|
|
||||||
private lateinit var homeViewModel: HomeViewModel
|
|
||||||
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||||
|
|
||||||
data class SettingsState(
|
data class SettingsState(
|
||||||
@@ -57,12 +36,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val showKpmInfo: Boolean = false
|
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
|
private var isInitialized = false
|
||||||
|
|
||||||
override fun attachBaseContext(newBase: Context) {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
@@ -72,11 +46,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
try {
|
try {
|
||||||
// 确保应用正确的语言设置
|
// 应用主题配置
|
||||||
LocaleUtils.applyLanguageSetting(this)
|
ThemeUtils.applyFullThemeConfiguration(this)
|
||||||
|
|
||||||
// 应用自定义 DPI
|
|
||||||
DisplayUtils.applyCustomDpi(this)
|
|
||||||
|
|
||||||
// Enable edge to edge
|
// Enable edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -89,146 +60,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
// 使用标记控制初始化流程
|
// 使用标记控制初始化流程
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
initializeViewModels()
|
lifecycleScope.launch {
|
||||||
initializeData()
|
ActivityInitializer.initialize(this@MainActivity, settingsStateFlow)
|
||||||
|
}
|
||||||
|
ThemeUtils.registerThemeChangeObserver(this)
|
||||||
isInitialized = true
|
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 {
|
setContent {
|
||||||
KernelSUTheme {
|
KernelSUTheme {
|
||||||
val navController = rememberNavController()
|
MainScreenContent()
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -236,96 +77,69 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList<Uri>) {
|
@Composable
|
||||||
try {
|
private fun MainScreenContent() {
|
||||||
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(this, zipUris)
|
val navController = rememberNavController()
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||||
|
val navigator = navController.rememberDestinationsNavigator()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
// 处理ZIP文件
|
||||||
if (zipFileInfos.isNotEmpty()) {
|
var zipUri by remember { mutableStateOf<ArrayList<Uri>?>(null) }
|
||||||
pendingZipFiles.value = zipFileInfos
|
|
||||||
showConfirmationDialog.value = true
|
// 在 LaunchedEffect 中处理 ZIP 文件
|
||||||
} else {
|
LaunchedEffect(Unit) {
|
||||||
finish()
|
zipUri = ZipFileManager.handleZipFiles(intent)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
InstallConfirmationDialog(
|
||||||
withContext(Dispatchers.Main) {
|
show = ZipFileManager.showConfirmationDialog.value,
|
||||||
|
zipFiles = ZipFileManager.pendingZipFiles.value,
|
||||||
|
onConfirm = { confirmedFiles ->
|
||||||
|
ZipFileManager.navigateToFlashScreen(
|
||||||
|
this@MainActivity,
|
||||||
|
confirmedFiles,
|
||||||
|
navigator,
|
||||||
|
lifecycleScope
|
||||||
|
)
|
||||||
|
ZipFileManager.clearZipFileState()
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
ZipFileManager.clearZipFileState()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
e.printStackTrace()
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(zipUri) {
|
||||||
|
zipUri?.let { uris ->
|
||||||
|
ZipFileManager.detectZipTypeAndShowConfirmation(this@MainActivity, uris)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToFlashScreen(
|
val showBottomBar = NavigationUtils.shouldShowBottomBar(currentDestination?.route)
|
||||||
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 }
|
|
||||||
|
|
||||||
when {
|
CompositionLocalProvider(
|
||||||
// 内核文件
|
LocalSnackbarHost provides snackBarHostState
|
||||||
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
) {
|
||||||
if (kernelUris.size == 1 && rootAvailable()) {
|
Scaffold(
|
||||||
navigator.navigate(
|
bottomBar = {
|
||||||
InstallScreenDestination(
|
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||||
preselectedKernelUri = kernelUris.first().toString()
|
showBottomBar = showBottomBar,
|
||||||
)
|
content = { BottomBar(navController) }
|
||||||
)
|
|
||||||
}
|
|
||||||
setAutoExitAfterFlash()
|
|
||||||
}
|
|
||||||
// 模块文件
|
|
||||||
moduleUris.isNotEmpty() -> {
|
|
||||||
navigator.navigate(
|
|
||||||
FlashScreenDestination(
|
|
||||||
FlashIt.FlashModules(ArrayList(moduleUris))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
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() {
|
override fun onResume() {
|
||||||
try {
|
try {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
@@ -343,12 +157,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun refreshData() {
|
private fun refreshData() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
ViewModelManager.refreshViewModelData()
|
||||||
superUserViewModel.fetchAppList()
|
DataRefreshUtils.refreshData(lifecycleScope)
|
||||||
DataRefreshUtils.refreshData(lifecycleScope)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +173,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
try {
|
try {
|
||||||
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
ThemeUtils.unregisterThemeChangeObserver(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
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 android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -17,12 +17,11 @@ import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
|||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import com.sukisu.ultra.ui.MainActivity
|
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.screen.BottomBarDestination
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
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")
|
@SuppressLint("ContextCastToActivity")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -40,9 +39,9 @@ fun BottomBar(navController: NavHostController) {
|
|||||||
val showKpmInfo = settings.showKpmInfo
|
val showKpmInfo = settings.showKpmInfo
|
||||||
|
|
||||||
// 收集计数数据
|
// 收集计数数据
|
||||||
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
|
val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState()
|
||||||
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
|
val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState()
|
||||||
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
|
val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
NavigationBar(
|
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.content.Context
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
@@ -19,8 +19,16 @@ class ThemeChangeContentObserver(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题管理工具类
|
||||||
|
*/
|
||||||
object ThemeUtils {
|
object ThemeUtils {
|
||||||
|
|
||||||
|
private var themeChangeObserver: ThemeChangeContentObserver? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化主题设置
|
||||||
|
*/
|
||||||
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
||||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||||
@@ -45,6 +53,9 @@ object ThemeUtils {
|
|||||||
CardConfig.load(activity.applicationContext)
|
CardConfig.load(activity.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册主题变化观察者
|
||||||
|
*/
|
||||||
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
||||||
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
@@ -61,13 +72,23 @@ object ThemeUtils {
|
|||||||
contentObserver
|
contentObserver
|
||||||
)
|
)
|
||||||
|
|
||||||
|
themeChangeObserver = contentObserver
|
||||||
return 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) {
|
fun onActivityPause(activity: MainActivity) {
|
||||||
CardConfig.save(activity.applicationContext)
|
CardConfig.save(activity.applicationContext)
|
||||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||||
@@ -76,21 +97,39 @@ object ThemeUtils {
|
|||||||
ThemeConfig.preventBackgroundRefresh = true
|
ThemeConfig.preventBackgroundRefresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity恢复时的主题处理
|
||||||
|
*/
|
||||||
fun onActivityResume() {
|
fun onActivityResume() {
|
||||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
loadCustomBackground()
|
loadCustomBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用完整的主题配置到Activity
|
||||||
|
*/
|
||||||
|
fun applyFullThemeConfiguration(activity: MainActivity) {
|
||||||
|
// 确保应用正确的语言设置
|
||||||
|
LocaleUtils.applyLanguageSetting(activity)
|
||||||
|
|
||||||
|
// 应用自定义 DPI
|
||||||
|
DisplayUtils.applyCustomDpi(activity)
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadThemeMode() {
|
private fun loadThemeMode() {
|
||||||
|
// 主题模式加载逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadThemeColors() {
|
private fun loadThemeColors() {
|
||||||
|
// 主题颜色加载逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDynamicColorState() {
|
private fun loadDynamicColorState() {
|
||||||
|
// 动态颜色状态加载逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCustomBackground() {
|
private fun loadCustomBackground() {
|
||||||
|
// 自定义背景加载逻辑
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,8 @@ import java.io.File
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.ui.util.module.ModuleOperationUtils
|
||||||
|
import com.sukisu.ultra.ui.util.module.ModuleUtils
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author ShirkNeko
|
* @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.CardConfig.cardElevation
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.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.checkNewVersion
|
||||||
import com.sukisu.ultra.ui.util.getSuSFS
|
import com.sukisu.ultra.ui.util.getSuSFS
|
||||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
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.R
|
||||||
import com.sukisu.ultra.getKernelVersion
|
import com.sukisu.ultra.getKernelVersion
|
||||||
import com.sukisu.ultra.ui.component.DialogHandle
|
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.rememberConfirmDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
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.getCardColors
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
import com.sukisu.ultra.ui.util.*
|
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.viewmodel.ModuleViewModel
|
||||||
import com.sukisu.ultra.ui.webui.WebUIActivity
|
import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||||
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
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.FabMenuPresets
|
||||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
import com.sukisu.ultra.ui.component.VerticalExpandableFab
|
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.AppCategory
|
||||||
import com.sukisu.ultra.ui.viewmodel.SortType
|
import com.sukisu.ultra.ui.viewmodel.SortType
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
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.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.*
|
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.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.sukisu.ultra.R
|
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.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
|
||||||
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
|
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158
|
||||||
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion159
|
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159
|
||||||
import com.sukisu.ultra.ui.util.isAbDevice
|
import com.sukisu.ultra.ui.util.isAbDevice
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -628,8 +640,21 @@ fun SuSFSConfigScreen(
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
val success = if (editingKstatConfig != null) {
|
val success = if (editingKstatConfig != null) {
|
||||||
SuSFSManager.editKstatConfig(
|
SuSFSManager.editKstatConfig(
|
||||||
context, editingKstatConfig!!, path, ino, dev, nlink, size, atime, atimeNsec,
|
context,
|
||||||
mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize
|
editingKstatConfig!!,
|
||||||
|
path,
|
||||||
|
ino,
|
||||||
|
dev,
|
||||||
|
nlink,
|
||||||
|
size,
|
||||||
|
atime,
|
||||||
|
atimeNsec,
|
||||||
|
mtime,
|
||||||
|
mtimeNsec,
|
||||||
|
ctime,
|
||||||
|
ctimeNsec,
|
||||||
|
blocks,
|
||||||
|
blksize
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
SuSFSManager.addKstatStatically(
|
SuSFSManager.addKstatStatically(
|
||||||
@@ -1251,7 +1276,11 @@ fun SuSFSConfigScreen(
|
|||||||
onToggleHideSusMountsForAllProcs = { hideForAll ->
|
onToggleHideSusMountsForAllProcs = { hideForAll ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
if (SuSFSManager.setHideSusMountsForAllProcs(context, hideForAll)) {
|
if (SuSFSManager.setHideSusMountsForAllProcs(
|
||||||
|
context,
|
||||||
|
hideForAll
|
||||||
|
)
|
||||||
|
) {
|
||||||
hideSusMountsForAllProcs = hideForAll
|
hideSusMountsForAllProcs = hideForAll
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@@ -1282,7 +1311,8 @@ fun SuSFSConfigScreen(
|
|||||||
onToggleUmountForZygoteIsoService = { enabled ->
|
onToggleUmountForZygoteIsoService = { enabled ->
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
val success = SuSFSManager.setUmountForZygoteIsoService(context, enabled)
|
val success =
|
||||||
|
SuSFSManager.setUmountForZygoteIsoService(context, enabled)
|
||||||
if (success) {
|
if (success) {
|
||||||
umountForZygoteIsoService = enabled
|
umountForZygoteIsoService = enabled
|
||||||
}
|
}
|
||||||
@@ -1393,7 +1423,7 @@ private fun BasicSettingsContent(
|
|||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
onAutoStartToggle: (Boolean) -> Unit,
|
onAutoStartToggle: (Boolean) -> Unit,
|
||||||
onShowSlotInfo: () -> Unit,
|
onShowSlotInfo: () -> Unit,
|
||||||
context: android.content.Context,
|
context: Context,
|
||||||
onShowBackupDialog: () -> Unit,
|
onShowBackupDialog: () -> Unit,
|
||||||
onShowRestoreDialog: () -> Unit,
|
onShowRestoreDialog: () -> Unit,
|
||||||
enableHideBl: Boolean,
|
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.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
@@ -29,7 +29,7 @@ import coil.compose.AsyncImage
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||||
import com.sukisu.ultra.R
|
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 com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
import kotlinx.coroutines.launch
|
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 android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.sukisu.ultra.R
|
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.util.SuSFSManager.isSusVersion158
|
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
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.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
||||||
@@ -19,9 +20,13 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import androidx.core.content.edit
|
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 com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -112,7 +117,7 @@ object SuSFSManager {
|
|||||||
configurationsJson.keys().forEach { key ->
|
configurationsJson.keys().forEach { key ->
|
||||||
val value = configurationsJson.get(key)
|
val value = configurationsJson.get(key)
|
||||||
configurations[key] = when (value) {
|
configurations[key] = when (value) {
|
||||||
is org.json.JSONArray -> {
|
is JSONArray -> {
|
||||||
val set = mutableSetOf<String>()
|
val set = mutableSetOf<String>()
|
||||||
for (i in 0 until value.length()) {
|
for (i in 0 until value.length()) {
|
||||||
set.add(value.getString(i))
|
set.add(value.getString(i))
|
||||||
@@ -304,7 +309,7 @@ object SuSFSManager {
|
|||||||
fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) {
|
fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) {
|
||||||
getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) }
|
getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) }
|
||||||
if (isAutoStartEnabled(context)) {
|
if (isAutoStartEnabled(context)) {
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
updateMagiskModule(context)
|
updateMagiskModule(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,7 +557,7 @@ object SuSFSManager {
|
|||||||
// 获取设备信息
|
// 获取设备信息
|
||||||
private fun getDeviceInfo(): String {
|
private fun getDeviceInfo(): String {
|
||||||
return try {
|
return try {
|
||||||
"${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.Build.VERSION.RELEASE})"
|
"${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})"
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"Unknown Device"
|
"Unknown Device"
|
||||||
}
|
}
|
||||||
@@ -1359,7 +1364,7 @@ object SuSFSManager {
|
|||||||
if (success) {
|
if (success) {
|
||||||
saveAndroidDataPath(context, path)
|
saveAndroidDataPath(context, path)
|
||||||
if (isAutoStartEnabled(context)) {
|
if (isAutoStartEnabled(context)) {
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
updateMagiskModule(context)
|
updateMagiskModule(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1373,7 +1378,7 @@ object SuSFSManager {
|
|||||||
if (success) {
|
if (success) {
|
||||||
saveSdcardPath(context, path)
|
saveSdcardPath(context, path)
|
||||||
if (isAutoStartEnabled(context)) {
|
if (isAutoStartEnabled(context)) {
|
||||||
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
updateMagiskModule(context)
|
updateMagiskModule(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.sukisu.ultra.ui.util
|
package com.sukisu.ultra.ui.susfs.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ import androidx.core.content.edit
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import coil.compose.AsyncImagePainter
|
import coil.compose.AsyncImagePainter
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
|
||||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
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 android.net.Uri
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
@@ -32,9 +32,10 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
|
||||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -67,9 +68,9 @@ fun ImageEditorDialog(
|
|||||||
)
|
)
|
||||||
val updateTransformation = remember {
|
val updateTransformation = remember {
|
||||||
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
||||||
val scaleDiff = kotlin.math.abs(newScale - lastScale)
|
val scaleDiff = abs(newScale - lastScale)
|
||||||
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
|
val offsetXDiff = abs(newOffsetX - lastOffsetX)
|
||||||
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
|
val offsetYDiff = abs(newOffsetY - lastOffsetY)
|
||||||
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
||||||
scale = newScale
|
scale = newScale
|
||||||
offsetX = newOffsetX
|
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.ContentResolver
|
||||||
import android.content.Context
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.reboot
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -159,7 +163,8 @@ object ModuleModify {
|
|||||||
val moduleDir = "/data/adb/modules"
|
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 ->
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
input.copyTo(process.outputStream)
|
input.copyTo(process.outputStream)
|
||||||
@@ -277,7 +282,11 @@ object ModuleModify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} 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) {
|
withContext(Dispatchers.Main) {
|
||||||
snackBarHost.showSnackbar(
|
snackBarHost.showSnackbar(
|
||||||
context.getString(R.string.allowlist_restore_failed, e.message),
|
context.getString(R.string.allowlist_restore_failed, e.message),
|
||||||
@@ -292,11 +301,11 @@ object ModuleModify {
|
|||||||
fun rememberModuleBackupLauncher(
|
fun rememberModuleBackupLauncher(
|
||||||
context: Context,
|
context: Context,
|
||||||
snackBarHost: SnackbarHostState,
|
snackBarHost: SnackbarHostState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
scope: CoroutineScope = rememberCoroutineScope()
|
||||||
) = rememberLauncherForActivityResult(
|
) = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { uri ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
backupModules(context, snackBarHost, uri)
|
backupModules(context, snackBarHost, uri)
|
||||||
@@ -309,8 +318,8 @@ object ModuleModify {
|
|||||||
fun rememberModuleRestoreLauncher(
|
fun rememberModuleRestoreLauncher(
|
||||||
context: Context,
|
context: Context,
|
||||||
snackBarHost: SnackbarHostState,
|
snackBarHost: SnackbarHostState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
scope: CoroutineScope = rememberCoroutineScope()
|
||||||
): androidx.activity.result.ActivityResultLauncher<Intent> {
|
): ActivityResultLauncher<Intent> {
|
||||||
var showRestoreDialog by remember { mutableStateOf(false) }
|
var showRestoreDialog by remember { mutableStateOf(false) }
|
||||||
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
|
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
|
||||||
|
|
||||||
@@ -330,7 +339,7 @@ object ModuleModify {
|
|||||||
return rememberLauncherForActivityResult(
|
return rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { uri ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val confirmResult = CompletableDeferred<Boolean>()
|
val confirmResult = CompletableDeferred<Boolean>()
|
||||||
@@ -353,11 +362,11 @@ object ModuleModify {
|
|||||||
fun rememberAllowlistBackupLauncher(
|
fun rememberAllowlistBackupLauncher(
|
||||||
context: Context,
|
context: Context,
|
||||||
snackBarHost: SnackbarHostState,
|
snackBarHost: SnackbarHostState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
scope: CoroutineScope = rememberCoroutineScope()
|
||||||
) = rememberLauncherForActivityResult(
|
) = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { uri ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
backupAllowlist(context, snackBarHost, uri)
|
backupAllowlist(context, snackBarHost, uri)
|
||||||
@@ -370,10 +379,14 @@ object ModuleModify {
|
|||||||
fun rememberAllowlistRestoreLauncher(
|
fun rememberAllowlistRestoreLauncher(
|
||||||
context: Context,
|
context: Context,
|
||||||
snackBarHost: SnackbarHostState,
|
snackBarHost: SnackbarHostState,
|
||||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
scope: CoroutineScope = rememberCoroutineScope()
|
||||||
): androidx.activity.result.ActivityResultLauncher<Intent> {
|
): ActivityResultLauncher<Intent> {
|
||||||
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
|
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
|
||||||
var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
|
var allowlistRestoreConfirmResult by remember {
|
||||||
|
mutableStateOf<CompletableDeferred<Boolean>?>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 显示允许列表恢复确认对话框
|
// 显示允许列表恢复确认对话框
|
||||||
AllowlistRestoreConfirmationDialog(
|
AllowlistRestoreConfirmationDialog(
|
||||||
@@ -391,7 +404,7 @@ object ModuleModify {
|
|||||||
return rememberLauncherForActivityResult(
|
return rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { uri ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val confirmResult = CompletableDeferred<Boolean>()
|
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.Context
|
||||||
import android.content.Intent
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ui.util.getRootShell
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
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.HanziToPinyin
|
||||||
import com.sukisu.ultra.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
import com.sukisu.ultra.ui.util.getRootShell
|
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 kotlinx.coroutines.withContext
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -42,9 +42,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import zako.zako.zako.zakoui.flash.FlashState
|
import zako.zako.zako.zakoui.screen.kernelFlash.state.FlashState
|
||||||
import zako.zako.zako.zakoui.flash.HorizonKernelState
|
import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelState
|
||||||
import zako.zako.zako.zakoui.flash.HorizonKernelWorker
|
import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelWorker
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.annotation.SuppressLint
|
||||||
import android.app.Activity
|
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.app.Activity
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
Reference in New Issue
Block a user