manager: Optimize the function of app classification and sorting method
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -395,4 +395,5 @@
|
||||
<!-- BottomSheet相关 -->
|
||||
<string name="menu_options">菜单选项</string>
|
||||
<string name="sort_options">排序方式</string>
|
||||
<string name="app_categories">应用类型选择</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user