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.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign 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.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
@@ -53,27 +54,17 @@ import com.sukisu.ultra.ui.component.VerticalExpandableFab
import com.sukisu.ultra.ui.component.FabMenuPresets import com.sukisu.ultra.ui.component.FabMenuPresets
import com.sukisu.ultra.ui.util.ModuleModify import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel 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.dergoogler.mmrl.ui.component.LabelItem
import com.sukisu.ultra.ui.theme.getCardElevation
import kotlin.math.* import kotlin.math.*
import java.io.File
// 应用分类 // 应用优先级枚举
enum class AppCategory(val displayNameRes: Int) { enum class AppPriority(val value: Int) {
ALL(R.string.category_all_apps), ROOT(1), // root权限应用
ROOT(R.string.category_root_apps), CUSTOM(2), // 自定义应用
CUSTOM(R.string.category_custom_apps), DEFAULT(3) // 默认应用
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)
} }
// 菜单项数据类 // 菜单项数据类
@@ -83,6 +74,39 @@ data class BottomSheetMenuItem(
val onClick: () -> Unit 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 * @author ShirkNeko
* @date 2025/6/8 * @date 2025/6/8
@@ -98,9 +122,9 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current val context = LocalContext.current
val snackBarHostState = remember { SnackbarHostState() } val snackBarHostState = remember { SnackbarHostState() }
// 分类和排序状态 // 使用ViewModel中的状态这些状态现在都会从SharedPreferences中加载并自动保存
var selectedCategory by remember { mutableStateOf(AppCategory.ALL) } val selectedCategory = viewModel.selectedCategory
var currentSortType by remember { mutableStateOf(SortType.NAME_ASC) } val currentSortType = viewModel.currentSortType
// BottomSheet状态 // BottomSheet状态
val bottomSheetState = rememberModalBottomSheetState( 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) { val filteredAndSortedApps = remember(viewModel.appList, selectedCategory, currentSortType, viewModel.search) {
var apps = viewModel.appList var apps = viewModel.appList
@@ -138,20 +169,75 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
AppCategory.DEFAULT -> apps.filter { !it.allowSu && !it.hasCustomProfile } AppCategory.DEFAULT -> apps.filter { !it.allowSu && !it.hasCustomProfile }
} }
// 按排序方式排序 // 优先级排序 + 二次排序
apps = when (currentSortType) { apps = apps.sortedWith { app1, app2 ->
SortType.NAME_ASC -> apps.sortedBy { it.label.lowercase() } val priority1 = getAppPriority(app1)
SortType.NAME_DESC -> apps.sortedByDescending { it.label.lowercase() } val priority2 = getAppPriority(app2)
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 } val priorityComparison = priority1.value.compareTo(priority2.value)
SortType.SIZE_ASC -> apps.sortedBy { it.packageInfo.applicationInfo?.let { context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir.length } ?: 0 }
SortType.USAGE_FREQ -> apps 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 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菜单项 // BottomSheet菜单项
val bottomSheetMenuItems = remember { val bottomSheetMenuItems = remember {
listOf( listOf(
@@ -206,10 +292,45 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
) )
} }
// 记录FAB展开状态用于图标动画
var isFabExpanded by remember { mutableStateOf(false) }
Scaffold( Scaffold(
topBar = { topBar = {
SearchAppBar( 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, searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it }, onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" }, onClearClick = { viewModel.search = "" },
@@ -285,115 +406,92 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
buttonSpacing = 72.dp, buttonSpacing = 72.dp,
animationDurationMs = 300, animationDurationMs = 300,
staggerDelayMs = 50, staggerDelayMs = 50,
mainButtonIcon = Icons.Filled.Add, // 根据模式选择不同的图标
mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
getMultiSelectMainIcon(isFabExpanded)
} else {
getSingleSelectMainIcon(isFabExpanded)
},
mainButtonExpandedIcon = Icons.Filled.Close mainButtonExpandedIcon = Icons.Filled.Close
) )
} }
) { innerPadding -> ) { innerPadding ->
Row( // 主要内容
PullToRefreshBox(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
) { ) {
// 主要内容区域 LazyColumn(
PullToRefreshBox( state = listState,
modifier = Modifier.weight(1f), modifier = Modifier
onRefresh = { .fillMaxSize()
scope.launch { viewModel.fetchAppList() } .nestedScroll(scrollBehavior.nestedScrollConnection),
}, verticalArrangement = Arrangement.spacedBy(6.dp),
isRefreshing = viewModel.isRefreshing contentPadding = PaddingValues(vertical = 8.dp)
) { ) {
LazyColumn( items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app ->
state = listState, AppItem(
modifier = Modifier app = app,
.fillMaxSize() isSelected = viewModel.selectedApps.contains(app.packageName),
.nestedScroll(scrollBehavior.nestedScrollConnection), onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
contentPadding = PaddingValues( onClick = {
start = 16.dp, if (viewModel.showBatchActions) {
end = 8.dp, viewModel.toggleAppSelection(app.packageName)
top = 16.dp, } else {
bottom = 16.dp navigator.navigate(AppProfileScreenDestination(app))
), }
verticalArrangement = Arrangement.spacedBy(4.dp) },
) { onLongClick = {
items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app -> if (!viewModel.showBatchActions) {
AppItem( viewModel.toggleBatchMode()
app = app, viewModel.toggleAppSelection(app.packageName)
isSelected = viewModel.selectedApps.contains(app.packageName), }
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, },
onClick = { viewModel = viewModel
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()) { if (filteredAndSortedApps.isEmpty()) {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(400.dp), .height(400.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
Column( Icon(
horizontalAlignment = Alignment.CenterHorizontally, imageVector = Icons.Filled.Archive,
verticalArrangement = Arrangement.Center contentDescription = null,
) { tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
Icon( modifier = Modifier
imageVector = Icons.Filled.Archive, .size(96.dp)
contentDescription = null, .padding(bottom = 16.dp)
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), )
modifier = Modifier Text(
.size(96.dp) text = if (selectedCategory == AppCategory.ALL) {
.padding(bottom = 16.dp) stringResource(R.string.no_apps_found)
) } else {
Text( stringResource(R.string.no_apps_in_category)
text = if (selectedCategory == AppCategory.ALL) { },
stringResource(R.string.no_apps_found) textAlign = TextAlign.Center,
} else { style = MaterialTheme.typography.bodyLarge,
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 // BottomSheet
@@ -422,12 +520,22 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
menuItems = bottomSheetMenuItems, menuItems = bottomSheetMenuItems,
currentSortType = currentSortType, currentSortType = currentSortType,
onSortTypeChanged = { newSortType -> onSortTypeChanged = { newSortType ->
currentSortType = newSortType viewModel.updateCurrentSortType(newSortType)
scope.launch { scope.launch {
bottomSheetState.hide() bottomSheetState.hide()
showBottomSheet = false 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( private fun BottomSheetContent(
menuItems: List<BottomSheetMenuItem>, menuItems: List<BottomSheetMenuItem>,
currentSortType: SortType, currentSortType: SortType,
onSortTypeChanged: (SortType) -> Unit onSortTypeChanged: (SortType) -> Unit,
selectedCategory: AppCategory,
onCategorySelected: (AppCategory) -> Unit,
appCounts: Map<AppCategory, Int>
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -453,6 +564,7 @@ private fun BottomSheetContent(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) )
// 菜单选项网格
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(4), columns = GridCells.Fixed(4),
modifier = Modifier.fillMaxWidth(), 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) @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AppItem( private fun AppItem(
@@ -706,31 +806,37 @@ private fun AppItem(
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.scale(scale) .scale(scale)
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onLongPress = { onLongClick() }, onLongPress = { onLongClick() },
onTap = { onClick() } onTap = { onClick() }
) )
}, }
.padding(horizontal = 8.dp),
headlineContent = { headlineContent = {
Text( Text(
text = app.label, text = app.label,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
}, },
supportingContent = { supportingContent = {
Column { Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text( Text(
text = app.packageName, text = app.packageName,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
FlowRow( 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) { if (app.allowSu) {
LabelItem(text = stringResource(R.string.label_root)) LabelItem(text = stringResource(R.string.label_root))

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.viewmodel package com.sukisu.ultra.ui.viewmodel
import android.content.Context import android.content.Context
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.Parcelable import android.os.Parcelable
@@ -27,6 +28,36 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import androidx.core.content.edit 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 * @author ShirkNeko
@@ -37,6 +68,10 @@ class SuperUserViewModel : ViewModel() {
companion object { companion object {
private const val TAG = "SuperUserViewModel" private const val TAG = "SuperUserViewModel"
var apps by mutableStateOf<List<AppInfo>>(emptyList()) 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 @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 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 private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
@@ -78,9 +121,81 @@ class SuperUserViewModel : ViewModel() {
var selectedApps by mutableStateOf<Set<String>>(emptySet()) var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set 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) { fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue 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 { private val sortedList by derivedStateOf {

View File

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

View File

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