manager: Optimize the function of app classification and sorting method

This commit is contained in:
ShirkNeko
2025-06-09 01:27:33 +08:00
parent 73dea0b8e7
commit 7b6f451cfb
4 changed files with 489 additions and 266 deletions

View File

@@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -53,27 +54,17 @@ import com.sukisu.ultra.ui.component.VerticalExpandableFab
import com.sukisu.ultra.ui.component.FabMenuPresets
import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.viewmodel.AppCategory
import com.sukisu.ultra.ui.viewmodel.SortType
import com.dergoogler.mmrl.ui.component.LabelItem
import com.sukisu.ultra.ui.theme.getCardElevation
import kotlin.math.*
import java.io.File
// 应用分类
enum class AppCategory(val displayNameRes: Int) {
ALL(R.string.category_all_apps),
ROOT(R.string.category_root_apps),
CUSTOM(R.string.category_custom_apps),
DEFAULT(R.string.category_default_apps)
}
// 排序方式
enum class SortType(val displayNameRes: Int) {
NAME_ASC(R.string.sort_name_asc),
NAME_DESC(R.string.sort_name_desc),
INSTALL_TIME_NEW(R.string.sort_install_time_new),
INSTALL_TIME_OLD(R.string.sort_install_time_old),
SIZE_DESC(R.string.sort_size_desc),
SIZE_ASC(R.string.sort_size_asc),
USAGE_FREQ(R.string.sort_usage_freq)
// 应用优先级枚举
enum class AppPriority(val value: Int) {
ROOT(1), // root权限应用
CUSTOM(2), // 自定义应用
DEFAULT(3) // 默认应用
}
// 菜单项数据类
@@ -83,6 +74,39 @@ data class BottomSheetMenuItem(
val onClick: () -> Unit
)
/**
* 获取应用的优先级
*/
private fun getAppPriority(app: SuperUserViewModel.AppInfo): AppPriority {
return when {
app.allowSu -> AppPriority.ROOT
app.hasCustomProfile -> AppPriority.CUSTOM
else -> AppPriority.DEFAULT
}
}
/**
* 获取多选模式的主按钮图标
*/
private fun getMultiSelectMainIcon(isExpanded: Boolean): ImageVector {
return if (isExpanded) {
Icons.Filled.Close
} else {
Icons.Filled.GridView
}
}
/**
* 获取单选模式的主按钮图标
*/
private fun getSingleSelectMainIcon(isExpanded: Boolean): ImageVector {
return if (isExpanded) {
Icons.Filled.Close
} else {
Icons.Filled.Add
}
}
/**
* @author ShirkNeko
* @date 2025/6/8
@@ -98,9 +122,9 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current
val snackBarHostState = remember { SnackbarHostState() }
// 分类和排序状态
var selectedCategory by remember { mutableStateOf(AppCategory.ALL) }
var currentSortType by remember { mutableStateOf(SortType.NAME_ASC) }
// 使用ViewModel中的状态这些状态现在都会从SharedPreferences中加载并自动保存
val selectedCategory = viewModel.selectedCategory
val currentSortType = viewModel.currentSortType
// BottomSheet状态
val bottomSheetState = rememberModalBottomSheetState(
@@ -126,6 +150,13 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
}
}
// 监听选中应用的变化,如果在多选模式下没有选中任何应用,则自动退出多选模式
LaunchedEffect(viewModel.selectedApps, viewModel.showBatchActions) {
if (viewModel.showBatchActions && viewModel.selectedApps.isEmpty()) {
viewModel.showBatchActions = false
}
}
// 应用分类和排序逻辑
val filteredAndSortedApps = remember(viewModel.appList, selectedCategory, currentSortType, viewModel.search) {
var apps = viewModel.appList
@@ -138,20 +169,75 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
AppCategory.DEFAULT -> apps.filter { !it.allowSu && !it.hasCustomProfile }
}
// 按排序方式排序
apps = when (currentSortType) {
SortType.NAME_ASC -> apps.sortedBy { it.label.lowercase() }
SortType.NAME_DESC -> apps.sortedByDescending { it.label.lowercase() }
SortType.INSTALL_TIME_NEW -> apps.sortedByDescending { it.packageInfo.firstInstallTime }
SortType.INSTALL_TIME_OLD -> apps.sortedBy { it.packageInfo.firstInstallTime }
SortType.SIZE_DESC -> apps.sortedByDescending { it.packageInfo.applicationInfo?.let { context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir.length } ?: 0 }
SortType.SIZE_ASC -> apps.sortedBy { it.packageInfo.applicationInfo?.let { context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir.length } ?: 0 }
SortType.USAGE_FREQ -> apps
// 优先级排序 + 二次排序
apps = apps.sortedWith { app1, app2 ->
val priority1 = getAppPriority(app1)
val priority2 = getAppPriority(app2)
// 首先按优先级排序
val priorityComparison = priority1.value.compareTo(priority2.value)
if (priorityComparison != 0) {
priorityComparison
} else {
// 在相同优先级内按指定排序方式排序
when (currentSortType) {
SortType.NAME_ASC -> app1.label.lowercase().compareTo(app2.label.lowercase())
SortType.NAME_DESC -> app2.label.lowercase().compareTo(app1.label.lowercase())
SortType.INSTALL_TIME_NEW -> app2.packageInfo.firstInstallTime.compareTo(app1.packageInfo.firstInstallTime)
SortType.INSTALL_TIME_OLD -> app1.packageInfo.firstInstallTime.compareTo(app2.packageInfo.firstInstallTime)
SortType.SIZE_DESC -> {
val size1: Long = app1.packageInfo.applicationInfo?.let {
try {
File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length()
} catch (_: Exception) {
0L
}
} ?: 0L
val size2: Long = app2.packageInfo.applicationInfo?.let {
try {
File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length()
} catch (_: Exception) {
0L
}
} ?: 0L
size2.compareTo(size1)
}
SortType.SIZE_ASC -> {
val size1: Long = app1.packageInfo.applicationInfo?.let {
try {
File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length()
} catch (_: Exception) {
0L
}
} ?: 0L
val size2: Long = app2.packageInfo.applicationInfo?.let {
try {
File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length()
} catch (_: Exception) {
0L
}
} ?: 0L
size1.compareTo(size2)
}
SortType.USAGE_FREQ -> app1.label.lowercase().compareTo(app2.label.lowercase()) // 默认按名称排序
}
}
}
apps
}
// 计算应用数量
val appCounts = remember(viewModel.appList) {
mapOf(
AppCategory.ALL to viewModel.appList.size,
AppCategory.ROOT to viewModel.appList.count { it.allowSu },
AppCategory.CUSTOM to viewModel.appList.count { !it.allowSu && it.hasCustomProfile },
AppCategory.DEFAULT to viewModel.appList.count { !it.allowSu && !it.hasCustomProfile }
)
}
// BottomSheet菜单项
val bottomSheetMenuItems = remember {
listOf(
@@ -206,10 +292,45 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
)
}
// 记录FAB展开状态用于图标动画
var isFabExpanded by remember { mutableStateOf(false) }
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.superuser)) },
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(stringResource(R.string.superuser))
// 显示当前分类和应用数量
if (selectedCategory != AppCategory.ALL) {
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.padding(start = 4.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(selectedCategory.displayNameRes),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "(${appCounts[selectedCategory] ?: 0})",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
},
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
@@ -285,115 +406,92 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
buttonSpacing = 72.dp,
animationDurationMs = 300,
staggerDelayMs = 50,
mainButtonIcon = Icons.Filled.Add,
// 根据模式选择不同的图标
mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
getMultiSelectMainIcon(isFabExpanded)
} else {
getSingleSelectMainIcon(isFabExpanded)
},
mainButtonExpandedIcon = Icons.Filled.Close
)
}
) { innerPadding ->
Row(
// 主要内容
PullToRefreshBox(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
) {
// 主要内容区域
PullToRefreshBox(
modifier = Modifier.weight(1f),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(6.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = PaddingValues(
start = 16.dp,
end = 8.dp,
top = 16.dp,
bottom = 16.dp
),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
// 当没有应用显示时显示空状态
if (filteredAndSortedApps.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentAlignment = Alignment.Center
// 当没有应用显示时显示空状态
if (filteredAndSortedApps.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Archive,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
text = if (selectedCategory == AppCategory.ALL) {
stringResource(R.string.no_apps_found)
} else {
stringResource(R.string.no_apps_in_category)
},
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
)
}
Icon(
imageVector = Icons.Filled.Archive,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
text = if (selectedCategory == AppCategory.ALL) {
stringResource(R.string.no_apps_found)
} else {
stringResource(R.string.no_apps_in_category)
},
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
}
}
// 右侧分类栏
CategorySidebar(
selectedCategory = selectedCategory,
onCategorySelected = { category ->
selectedCategory = category
// 切换分类时滚动到顶部
scope.launch {
listState.animateScrollToItem(0)
}
},
appCounts = mapOf(
AppCategory.ALL to viewModel.appList.size,
AppCategory.ROOT to viewModel.appList.count { it.allowSu },
AppCategory.CUSTOM to viewModel.appList.count { !it.allowSu && it.hasCustomProfile },
AppCategory.DEFAULT to viewModel.appList.count { !it.allowSu && !it.hasCustomProfile }
),
modifier = Modifier.width(86.dp)
)
}
// BottomSheet
@@ -422,12 +520,22 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
menuItems = bottomSheetMenuItems,
currentSortType = currentSortType,
onSortTypeChanged = { newSortType ->
currentSortType = newSortType
viewModel.updateCurrentSortType(newSortType)
scope.launch {
bottomSheetState.hide()
showBottomSheet = false
}
}
},
selectedCategory = selectedCategory,
onCategorySelected = { newCategory ->
viewModel.updateSelectedCategory(newCategory)
scope.launch {
listState.animateScrollToItem(0)
bottomSheetState.hide()
showBottomSheet = false
}
},
appCounts = appCounts
)
}
}
@@ -438,7 +546,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
private fun BottomSheetContent(
menuItems: List<BottomSheetMenuItem>,
currentSortType: SortType,
onSortTypeChanged: (SortType) -> Unit
onSortTypeChanged: (SortType) -> Unit,
selectedCategory: AppCategory,
onCategorySelected: (AppCategory) -> Unit,
appCounts: Map<AppCategory, Int>
) {
Column(
modifier = Modifier
@@ -453,6 +564,7 @@ private fun BottomSheetContent(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 菜单选项网格
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier.fillMaxWidth(),
@@ -491,6 +603,126 @@ private fun BottomSheetContent(
)
}
}
// 应用分类选项
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp))
Text(
text = stringResource(R.string.app_categories),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(AppCategory.entries.toTypedArray()) { category ->
CategoryChip(
category = category,
isSelected = selectedCategory == category,
onClick = { onCategorySelected(category) },
appCount = appCounts[category] ?: 0
)
}
}
}
}
@Composable
private fun CategoryChip(
category: AppCategory,
isSelected: Boolean,
onClick: () -> Unit,
appCount: Int,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "categoryChipScale"
)
Surface(
modifier = modifier
.fillMaxWidth()
.scale(scale)
.clickable(
interactionSource = interactionSource,
indication = null
) { onClick() },
shape = RoundedCornerShape(12.dp),
color = if (isSelected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
},
tonalElevation = if (isSelected) 4.dp else 0.dp
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 分类信息行
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = stringResource(category.displayNameRes),
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium
),
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// 选中指示器
AnimatedVisibility(
visible = isSelected,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = stringResource(R.string.selected),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(16.dp)
)
}
}
// 应用数量
Text(
text = "$appCount apps",
style = MaterialTheme.typography.labelSmall,
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
} else {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
}
)
}
}
}
@@ -548,138 +780,6 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) {
}
}
@Composable
private fun CategorySidebar(
selectedCategory: AppCategory,
onCategorySelected: (AppCategory) -> Unit,
appCounts: Map<AppCategory, Int>,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier.fillMaxHeight(),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 2.dp,
shape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 4.dp)
) {
items(AppCategory.entries.toTypedArray()) { category ->
CategoryItem(
category = category,
isSelected = selectedCategory == category,
appCount = appCounts[category] ?: 0,
onClick = { onCategorySelected(category) }
)
}
}
}
}
@Composable
private fun CategoryItem(
category: AppCategory,
isSelected: Boolean,
appCount: Int,
onClick: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val animatedScale by animateFloatAsState(
targetValue = when {
isPressed -> 0.96f
isSelected -> 1.0f
else -> 1.0f
},
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "categoryScale"
)
Card(
modifier = Modifier
.fillMaxWidth()
.scale(animatedScale)
.clickable(
interactionSource = interactionSource,
indication = null
) { onClick() },
colors = CardDefaults.cardColors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.surfaceBright
}
),
elevation = getCardElevation()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 分类图标
val iconVector = when (category) {
AppCategory.ALL -> Icons.Filled.Apps
AppCategory.ROOT -> Icons.Filled.Security
AppCategory.CUSTOM -> Icons.Filled.Settings
AppCategory.DEFAULT -> Icons.Filled.Smartphone
}
val iconColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurface
}
Icon(
imageVector = iconVector,
contentDescription = stringResource(category.displayNameRes),
tint = iconColor,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.height(2.dp))
// 分类名称
Text(
text = stringResource(category.displayNameRes),
style = MaterialTheme.typography.labelSmall,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurface
},
textAlign = TextAlign.Center,
maxLines = 2,
fontSize = 10.sp
)
// 应用数量
Text(
text = appCount.toString(),
style = MaterialTheme.typography.labelSmall,
color = if (isSelected) {
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
},
modifier = Modifier.padding(top = 1.dp),
fontSize = 9.sp
)
}
}
}
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun AppItem(
@@ -706,31 +806,37 @@ private fun AppItem(
ListItem(
modifier = Modifier
.fillMaxWidth()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { onLongClick() },
onTap = { onClick() }
)
},
}
.padding(horizontal = 8.dp),
headlineContent = {
Text(
text = app.label,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
supportingContent = {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = app.packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
FlowRow(
modifier = Modifier.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
if (app.allowSu) {
LabelItem(text = stringResource(R.string.label_root))

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.viewmodel
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Parcelable
@@ -27,6 +28,36 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import androidx.core.content.edit
// 应用分类
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
CUSTOM(com.sukisu.ultra.R.string.category_custom_apps, "CUSTOM"),
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
companion object {
fun fromPersistKey(key: String): AppCategory {
return entries.find { it.persistKey == key } ?: ALL
}
}
}
// 排序方式
enum class SortType(val displayNameRes: Int, val persistKey: String) {
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
INSTALL_TIME_NEW(com.sukisu.ultra.R.string.sort_install_time_new, "INSTALL_TIME_NEW"),
INSTALL_TIME_OLD(com.sukisu.ultra.R.string.sort_install_time_old, "INSTALL_TIME_OLD"),
SIZE_DESC(com.sukisu.ultra.R.string.sort_size_desc, "SIZE_DESC"),
SIZE_ASC(com.sukisu.ultra.R.string.sort_size_asc, "SIZE_ASC"),
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
companion object {
fun fromPersistKey(key: String): SortType {
return entries.find { it.persistKey == key } ?: NAME_ASC
}
}
}
/**
* @author ShirkNeko
@@ -37,6 +68,10 @@ class SuperUserViewModel : ViewModel() {
companion object {
private const val TAG = "SuperUserViewModel"
var apps by mutableStateOf<List<AppInfo>>(emptyList())
private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category"
private const val KEY_CURRENT_SORT_TYPE = "current_sort_type"
}
@Parcelize
@@ -64,10 +99,18 @@ class SuperUserViewModel : ViewModel() {
}
}
}
private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE)!!
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
var search by mutableStateOf("")
var showSystemApps by mutableStateOf(prefs.getBoolean("show_system_apps", false))
var showSystemApps by mutableStateOf(loadShowSystemApps())
private set
var selectedCategory by mutableStateOf(loadSelectedCategory())
private set
var currentSortType by mutableStateOf(loadCurrentSortType())
private set
var isRefreshing by mutableStateOf(false)
private set
@@ -78,9 +121,81 @@ class SuperUserViewModel : ViewModel() {
var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set
/**
* 从SharedPreferences加载显示系统应用设置
*/
private fun loadShowSystemApps(): Boolean {
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
}
/**
* 从SharedPreferences加载选择的应用分类
*/
private fun loadSelectedCategory(): AppCategory {
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey
return AppCategory.fromPersistKey(categoryKey)
}
/**
* 从SharedPreferences加载当前排序方式
*/
private fun loadCurrentSortType(): SortType {
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey
return SortType.fromPersistKey(sortKey)
}
/**
* 更新显示系统应用设置并保存到SharedPreferences
*/
fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue
prefs.edit { putBoolean("show_system_apps", newValue) }
saveShowSystemApps(newValue)
}
/**
* 更新选择的应用分类并保存到SharedPreferences
*/
fun updateSelectedCategory(newCategory: AppCategory) {
selectedCategory = newCategory
saveSelectedCategory(newCategory)
}
/**
* 更新当前排序方式并保存到SharedPreferences
*/
fun updateCurrentSortType(newSortType: SortType) {
currentSortType = newSortType
saveCurrentSortType(newSortType)
}
/**
* 保存显示系统应用设置到SharedPreferences
*/
private fun saveShowSystemApps(value: Boolean) {
prefs.edit {
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
}
Log.d(TAG, "Saved show system apps: $value")
}
/**
* 保存选择的应用分类到SharedPreferences
*/
private fun saveSelectedCategory(category: AppCategory) {
prefs.edit {
putString(KEY_SELECTED_CATEGORY, category.persistKey)
}
Log.d(TAG, "Saved selected category: ${category.persistKey}")
}
/**
* 保存当前排序方式到SharedPreferences
*/
private fun saveCurrentSortType(sortType: SortType) {
prefs.edit {
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
}
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
}
private val sortedList by derivedStateOf {

View File

@@ -395,4 +395,5 @@
<!-- BottomSheet相关 -->
<string name="menu_options">菜单选项</string>
<string name="sort_options">排序方式</string>
<string name="app_categories">应用类型选择</string>
</resources>

View File

@@ -397,4 +397,5 @@
<!-- BottomSheet相关 -->
<string name="menu_options">Menu Options</string>
<string name="sort_options">Sort by</string>
<string name="app_categories">Application Type Selection</string>
</resources>