From 7b6f451cfb68f1e8ab9dbfcb145db12f35910c49 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Mon, 9 Jun 2025 01:27:33 +0800 Subject: [PATCH] manager: Optimize the function of app classification and sorting method --- .../com/sukisu/ultra/ui/screen/SuperUser.kt | 632 ++++++++++-------- .../ultra/ui/viewmodel/SuperUserViewModel.kt | 121 +++- .../src/main/res/values-zh-rCN/strings.xml | 1 + manager/app/src/main/res/values/strings.xml | 1 + 4 files changed, 489 insertions(+), 266 deletions(-) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index 97cf332e..e0d04ce1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -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, currentSortType: SortType, - onSortTypeChanged: (SortType) -> Unit + onSortTypeChanged: (SortType) -> Unit, + selectedCategory: AppCategory, + onCategorySelected: (AppCategory) -> Unit, + appCounts: Map ) { 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, - 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)) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 34e8af65..a79371e1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -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>(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>(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 { diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 804ebdb1..868201ae 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -395,4 +395,5 @@ 菜单选项 排序方式 + 应用类型选择 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 7814891c..6eb6af32 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -397,4 +397,5 @@ Menu Options Sort by + Application Type Selection