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.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,20 +406,22 @@ 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),
|
||||||
) {
|
|
||||||
// 主要内容区域
|
|
||||||
PullToRefreshBox(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
scope.launch { viewModel.fetchAppList() }
|
scope.launch { viewModel.fetchAppList() }
|
||||||
},
|
},
|
||||||
@@ -309,13 +432,8 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
contentPadding = PaddingValues(
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
start = 16.dp,
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
end = 8.dp,
|
|
||||||
top = 16.dp,
|
|
||||||
bottom = 16.dp
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
) {
|
||||||
items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app ->
|
items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app ->
|
||||||
AppItem(
|
AppItem(
|
||||||
@@ -376,26 +494,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右侧分类栏
|
|
||||||
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
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user