diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt new file mode 100644 index 00000000..40d04db1 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt @@ -0,0 +1,277 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp +import com.sukisu.ultra.R + +// 菜单项数据类 +data class FabMenuItem( + val icon: ImageVector, + val labelRes: Int, + val color: Color = Color.Unspecified, + val onClick: () -> Unit +) + +// 动画配置 +object FabAnimationConfig { + const val ANIMATION_DURATION = 300 + const val STAGGER_DELAY = 50 + val BUTTON_SPACING = 72.dp + val BUTTON_SIZE = 56.dp + val SMALL_BUTTON_SIZE = 48.dp +} + +@Composable +fun VerticalExpandableFab( + menuItems: List, + modifier: Modifier = Modifier, + buttonSize: Dp = FabAnimationConfig.BUTTON_SIZE, + smallButtonSize: Dp = FabAnimationConfig.SMALL_BUTTON_SIZE, + buttonSpacing: Dp = FabAnimationConfig.BUTTON_SPACING, + animationDurationMs: Int = FabAnimationConfig.ANIMATION_DURATION, + staggerDelayMs: Int = FabAnimationConfig.STAGGER_DELAY, + mainButtonIcon: ImageVector = Icons.Filled.Add, + mainButtonExpandedIcon: ImageVector = Icons.Filled.Close, + onMainButtonClick: (() -> Unit)? = null, +) { + var isExpanded by remember { mutableStateOf(false) } + + // 主按钮旋转动画 + val rotationAngle by animateFloatAsState( + targetValue = if (isExpanded) 45f else 0f, + animationSpec = tween( + durationMillis = animationDurationMs, + easing = FastOutSlowInEasing + ), + label = "mainButtonRotation" + ) + + // 主按钮缩放动画 + val mainButtonScale by animateFloatAsState( + targetValue = if (isExpanded) 1.1f else 1f, + animationSpec = tween( + durationMillis = animationDurationMs, + easing = FastOutSlowInEasing + ), + label = "mainButtonScale" + ) + + Box( + modifier = modifier.wrapContentSize(), + contentAlignment = Alignment.BottomEnd + ) { + // 子菜单按钮 + menuItems.forEachIndexed { index, menuItem -> + val animatedOffsetY by animateFloatAsState( + targetValue = if (isExpanded) { + -(buttonSpacing.value * (index + 1)) + } else { + 0f + }, + animationSpec = tween( + durationMillis = animationDurationMs, + delayMillis = if (isExpanded) { + index * staggerDelayMs + } else { + (menuItems.size - index - 1) * staggerDelayMs + }, + easing = FastOutSlowInEasing + ), + label = "fabOffset$index" + ) + + val animatedScale by animateFloatAsState( + targetValue = if (isExpanded) 1f else 0f, + animationSpec = tween( + durationMillis = animationDurationMs, + delayMillis = if (isExpanded) { + index * staggerDelayMs + 100 + } else { + (menuItems.size - index - 1) * staggerDelayMs + }, + easing = FastOutSlowInEasing + ), + label = "fabScale$index" + ) + + val animatedAlpha by animateFloatAsState( + targetValue = if (isExpanded) 1f else 0f, + animationSpec = tween( + durationMillis = animationDurationMs, + delayMillis = if (isExpanded) { + index * staggerDelayMs + 150 + } else { + (menuItems.size - index - 1) * staggerDelayMs + }, + easing = FastOutSlowInEasing + ), + label = "fabAlpha$index" + ) + + // 子按钮容器(包含标签) + Row( + modifier = Modifier + .offset(y = animatedOffsetY.dp) + .scale(animatedScale) + .alpha(animatedAlpha), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + // 标签 + AnimatedVisibility( + visible = isExpanded && animatedScale > 0.5f, + enter = slideInHorizontally( + initialOffsetX = { it / 2 }, + animationSpec = tween(200) + ) + fadeIn(animationSpec = tween(200)), + exit = slideOutHorizontally( + targetOffsetX = { it / 2 }, + animationSpec = tween(150) + ) + fadeOut(animationSpec = tween(150)) + ) { + Surface( + modifier = Modifier.padding(end = 16.dp), + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.inverseSurface, + tonalElevation = 6.dp + ) { + Text( + text = stringResource(menuItem.labelRes), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.inverseOnSurface + ) + } + } + + // 子按钮 + SmallFloatingActionButton( + onClick = { + menuItem.onClick() + isExpanded = false + }, + modifier = Modifier.size(smallButtonSize), + containerColor = if (menuItem.color != Color.Unspecified) { + menuItem.color + } else { + MaterialTheme.colorScheme.secondary + }, + contentColor = if (menuItem.color != Color.Unspecified) { + if (menuItem.color == Color.Gray) Color.White + else MaterialTheme.colorScheme.onSecondary + } else { + MaterialTheme.colorScheme.onSecondary + }, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 4.dp, + pressedElevation = 6.dp + ) + ) { + Icon( + imageVector = menuItem.icon, + contentDescription = stringResource(menuItem.labelRes), + modifier = Modifier.size(20.dp) + ) + } + } + } + + // 主按钮 + FloatingActionButton( + onClick = { + onMainButtonClick?.invoke() + isExpanded = !isExpanded + }, + modifier = Modifier + .size(buttonSize) + .scale(mainButtonScale), + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 6.dp, + pressedElevation = 8.dp, + hoveredElevation = 8.dp + ) + ) { + Icon( + imageVector = if (isExpanded) mainButtonExpandedIcon else mainButtonIcon, + contentDescription = stringResource( + if (isExpanded) R.string.collapse_menu else R.string.expand_menu + ), + modifier = Modifier + .size(24.dp) + .rotate(if (mainButtonIcon == Icons.Filled.Add) rotationAngle else 0f) + ) + } + } +} + +// 预设菜单项 +object FabMenuPresets { + fun getScrollMenuItems( + onScrollToTop: () -> Unit, + onScrollToBottom: () -> Unit + ) = listOf( + FabMenuItem( + icon = Icons.Filled.KeyboardArrowUp, + labelRes = R.string.scroll_to_top, + onClick = onScrollToTop + ), + FabMenuItem( + icon = Icons.Filled.KeyboardArrowDown, + labelRes = R.string.scroll_to_bottom, + onClick = onScrollToBottom + ) + ) + + @Composable + fun getBatchActionMenuItems( + onCancel: () -> Unit, + onDeny: () -> Unit, + onAllow: () -> Unit, + onUnmountModules: () -> Unit, + onDisableUnmount: () -> Unit + ) = listOf( + FabMenuItem( + icon = Icons.Filled.Close, + labelRes = R.string.cancel, + color = Color.Gray, + onClick = onCancel + ), + FabMenuItem( + icon = Icons.Filled.Block, + labelRes = R.string.deny_authorization, + color = MaterialTheme.colorScheme.error, + onClick = onDeny + ), + FabMenuItem( + icon = Icons.Filled.Check, + labelRes = R.string.grant_authorization, + color = MaterialTheme.colorScheme.primary, + onClick = onAllow + ), + FabMenuItem( + icon = Icons.Filled.FolderOff, + labelRes = R.string.unmount_modules, + onClick = onUnmountModules + ), + FabMenuItem( + icon = Icons.Filled.Folder, + labelRes = R.string.disable_unmount, + onClick = onDisableUnmount + ) + ) +} \ No newline at end of file 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 5077afbb..5e0b21bd 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 @@ -8,10 +8,15 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.clickable import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -21,13 +26,16 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput 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.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel @@ -41,16 +49,44 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import com.sukisu.ultra.Natives import com.sukisu.ultra.ui.component.SearchAppBar +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.dergoogler.mmrl.ui.component.LabelItem -import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation +import kotlin.math.* + +// 应用分类 +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) +} + +// 菜单项数据类 +data class BottomSheetMenuItem( + val icon: ImageVector, + val titleRes: Int, + val onClick: () -> Unit +) /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/6/8 */ @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Destination @@ -63,6 +99,16 @@ 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) } + + // BottomSheet状态 + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + var showBottomSheet by remember { mutableStateOf(false) } + // 添加备份和还原启动器 val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState) val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState) @@ -81,6 +127,86 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { } } + // 应用分类和排序逻辑 + val filteredAndSortedApps = remember(viewModel.appList, selectedCategory, currentSortType, viewModel.search) { + var apps = viewModel.appList + + // 按分类筛选 + apps = when (selectedCategory) { + AppCategory.ALL -> apps + AppCategory.ROOT -> apps.filter { it.allowSu } + AppCategory.CUSTOM -> apps.filter { !it.allowSu && it.hasCustomProfile } + 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 + } + + // BottomSheet菜单项 + val bottomSheetMenuItems = remember { + listOf( + BottomSheetMenuItem( + icon = Icons.Filled.Refresh, + titleRes = R.string.refresh, + onClick = { + scope.launch { + viewModel.fetchAppList() + bottomSheetState.hide() + showBottomSheet = false + } + } + ), + BottomSheetMenuItem( + icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, + titleRes = if (viewModel.showSystemApps) { + R.string.hide_system_apps + } else { + R.string.show_system_apps + }, + onClick = { + viewModel.updateShowSystemApps(!viewModel.showSystemApps) + scope.launch { + bottomSheetState.hide() + showBottomSheet = false + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.Save, + titleRes = R.string.backup_allowlist, + onClick = { + backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) + scope.launch { + bottomSheetState.hide() + showBottomSheet = false + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.RestoreFromTrash, + titleRes = R.string.restore_allowlist, + onClick = { + restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) + scope.launch { + bottomSheetState.hide() + showBottomSheet = false + } + } + ) + ) + } + Scaffold( topBar = { SearchAppBar( @@ -89,84 +215,15 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { onSearchTextChange = { viewModel.search = it }, onClearClick = { viewModel.search = "" }, dropdownContent = { - var showDropdown by remember { mutableStateOf(false) } - IconButton( - onClick = { showDropdown = true }, + onClick = { + showBottomSheet = true + }, ) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings), ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem( - text = { Text(stringResource(R.string.refresh)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = null, - ) - }, - onClick = { - scope.launch { - viewModel.fetchAppList() - } - showDropdown = false - } - ) - DropdownMenuItem( - text = { - Text( - if (viewModel.showSystemApps) { - stringResource(R.string.hide_system_apps) - } else { - stringResource(R.string.show_system_apps) - } - ) - }, - leadingIcon = { - Icon( - imageVector = if (viewModel.showSystemApps) - Icons.Filled.VisibilityOff else Icons.Filled.Visibility, - contentDescription = null, - ) - }, - onClick = { - viewModel.updateShowSystemApps(!viewModel.showSystemApps) - showDropdown = false - } - ) - HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp)) - DropdownMenuItem( - text = { Text(stringResource(R.string.backup_allowlist)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = null, - ) - }, - onClick = { - backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) - showDropdown = false - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.restore_allowlist)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.RestoreFromTrash, - contentDescription = null, - ) - }, - onClick = { - restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) - showDropdown = false - } - ) - } } }, scrollBehavior = scrollBehavior @@ -175,344 +232,97 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { snackbarHost = { SnackbarHost(snackBarHostState) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), floatingActionButton = { - // 侧边悬浮按钮集合 - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.End - ) { - // 批量操作相关按钮 - // 只有在批量模式且有选中应用时才显示批量操作按钮 - if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - // 取消按钮 - val cancelInteractionSource = remember { MutableInteractionSource() } - val isCancelPressed by cancelInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { + VerticalExpandableFab( + menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + FabMenuPresets.getBatchActionMenuItems( + onCancel = { viewModel.selectedApps = emptySet() viewModel.showBatchActions = false }, - modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = cancelInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp), - containerColor = Color.Gray - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = stringResource(android.R.string.cancel), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isCancelPressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(android.R.string.cancel), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - - // 取消授权按钮 - val unauthorizeInteractionSource = remember { MutableInteractionSource() } - val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { + onDeny = { scope.launch { viewModel.updateBatchPermissions(false) } }, - modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = unauthorizeInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp), - containerColor = MaterialTheme.colorScheme.error - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Block, - contentDescription = stringResource(R.string.batch_cancel_authorization), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isUnauthorizePressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.batch_cancel_authorization), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - - // 授权按钮 - val authorizeInteractionSource = remember { MutableInteractionSource() } - val isAuthorizePressed by authorizeInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { + onAllow = { scope.launch { viewModel.updateBatchPermissions(true) } }, - modifier = Modifier.size(if (isAuthorizePressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = authorizeInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = stringResource(R.string.batch_authorization), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isAuthorizePressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.batch_authorization), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium + onUnmountModules = { + scope.launch { + viewModel.updateBatchPermissions( + allowSu = false, + umountModules = true + ) + } + }, + onDisableUnmount = { + scope.launch { + viewModel.updateBatchPermissions( + allowSu = false, + umountModules = false ) } } - } - - // 添加分隔 - Spacer(modifier = Modifier.height(8.dp)) - } - - if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - - // 在批量操作按钮组中添加卸载模块的按钮 - // 卸载模块启用按钮 - val umountEnableInteractionSource = remember { MutableInteractionSource() } - val isUmountEnablePressed by umountEnableInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, // 不改变ROOT权限状态 - umountModules = true // 启用卸载模块 - ) - } - }, - modifier = Modifier.size(if (isUmountEnablePressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = umountEnableInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.FolderOff, - contentDescription = stringResource(R.string.profile_umount_modules), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isUmountEnablePressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.profile_umount_modules), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - - // 卸载模块禁用按钮 - val umountDisableInteractionSource = remember { MutableInteractionSource() } - val isUmountDisablePressed by umountDisableInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, // 不改变ROOT权限状态 - umountModules = false // 禁用卸载模块 - ) - } - }, - modifier = Modifier.size(if (isUmountDisablePressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = umountDisableInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp), - containerColor = MaterialTheme.colorScheme.error - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Folder, - contentDescription = stringResource(R.string.profile_umount_modules_disable), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isUmountDisablePressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.profile_umount_modules_disable), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - // 添加分隔 - Spacer(modifier = Modifier.height(8.dp)) - } - } - - // 向上导航按钮 - val topBtnInteractionSource = remember { MutableInteractionSource() } - val isTopBtnPressed by topBtnInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { - scope.launch { - listState.animateScrollToItem(0) - } - }, - modifier = Modifier.size(if (isTopBtnPressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = topBtnInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.KeyboardArrowUp, - contentDescription = stringResource(R.string.scroll_to_top_description), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isTopBtnPressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.scroll_to_top), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - - // 向下导航按钮 - val bottomBtnInteractionSource = remember { MutableInteractionSource() } - val isBottomBtnPressed by bottomBtnInteractionSource.collectIsPressedAsState() - - FloatingActionButton( - onClick = { - scope.launch { - val lastIndex = viewModel.appList.size - 1 - if (lastIndex >= 0) { - listState.animateScrollToItem(lastIndex) + ) + } else { + FabMenuPresets.getScrollMenuItems( + onScrollToTop = { + scope.launch { + listState.animateScrollToItem(0) + } + }, + onScrollToBottom = { + scope.launch { + val lastIndex = filteredAndSortedApps.size - 1 + if (lastIndex >= 0) { + listState.animateScrollToItem(lastIndex) + } } } - }, - modifier = Modifier.size(if (isBottomBtnPressed) 56.dp else 40.dp), - shape = CircleShape, - interactionSource = bottomBtnInteractionSource, - elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.KeyboardArrowDown, - contentDescription = stringResource(R.string.scroll_to_bottom_description), - modifier = Modifier.size(24.dp) - ) - AnimatedVisibility( - visible = isBottomBtnPressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - stringResource(R.string.scroll_to_bottom), - modifier = Modifier.padding(end = 4.dp), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - } + ) + }, + buttonSpacing = 72.dp, + animationDurationMs = 300, + staggerDelayMs = 50, + mainButtonIcon = Icons.Filled.Add, + mainButtonExpandedIcon = Icons.Filled.Close + ) } ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, - isRefreshing = viewModel.isRefreshing + + Row( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - contentPadding = PaddingValues( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 16.dp - ), - verticalArrangement = Arrangement.spacedBy(16.dp) + // 主要内容区域 + PullToRefreshBox( + modifier = Modifier.weight(1f), + onRefresh = { + scope.launch { viewModel.fetchAppList() } + }, + isRefreshing = viewModel.isRefreshing ) { - // 获取分组后的应用列表 - val rootApps = viewModel.appList.filter { it.allowSu } - val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile } - val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile } - - // 显示ROOT权限应用组 - if (rootApps.isNotEmpty()) { - items(rootApps, key = { "root_" + it.packageName + it.uid }) { app -> + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = PaddingValues( + start = 16.dp, + end = 4.dp, + top = 16.dp, + bottom = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app -> AppItem( app = app, isSelected = viewModel.selectedApps.contains(app.packageName), onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.updateAppProfileLocally(app.packageName, updatedProfile) - } - } - }, onClick = { if (viewModel.showBatchActions) { viewModel.toggleAppSelection(app.packageName) @@ -521,7 +331,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { } }, onLongClick = { - // 长按进入多选模式 if (!viewModel.showBatchActions) { viewModel.toggleBatchMode() viewModel.toggleAppSelection(app.packageName) @@ -530,109 +339,347 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { viewModel = viewModel ) } - } - // 显示自定义配置应用组 - if (customApps.isNotEmpty()) { - items(customApps, key = { "custom_" + it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.updateAppProfileLocally(app.packageName, updatedProfile) - } - } - }, - 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 (otherApps.isNotEmpty()) { - items(otherApps, key = { "other_" + it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.updateAppProfileLocally(app.packageName, updatedProfile) - } - } - }, - 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 (viewModel.appList.isEmpty()) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .height(400.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + // 当没有应用显示时显示空状态 + if (filteredAndSortedApps.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(400.dp), + contentAlignment = Alignment.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 = stringResource(R.string.no_apps_found), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) + 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, + ) + } } } } } } + + // 右侧分类栏 + 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 + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = bottomSheetState, + dragHandle = { + Surface( + modifier = Modifier.padding(vertical = 11.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + shape = RoundedCornerShape(16.dp) + ) { + Box( + Modifier.size( + width = 32.dp, + height = 4.dp + ) + ) + } + } + ) { + BottomSheetContent( + menuItems = bottomSheetMenuItems, + currentSortType = currentSortType, + onSortTypeChanged = { newSortType -> + currentSortType = newSortType + scope.launch { + bottomSheetState.hide() + showBottomSheet = false + } + } + ) + } + } + } +} + +@Composable +private fun BottomSheetContent( + menuItems: List, + currentSortType: SortType, + onSortTypeChanged: (SortType) -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 24.dp) + ) { + // 标题 + Text( + text = stringResource(R.string.menu_options), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(menuItems) { menuItem -> + BottomSheetMenuItemView( + menuItem = menuItem + ) + } + } + + // 排序选项 + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) + + Text( + text = stringResource(R.string.sort_options), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) + ) + + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(SortType.entries.toTypedArray()) { sortType -> + FilterChip( + onClick = { onSortTypeChanged(sortType) }, + label = { Text(stringResource(sortType.displayNameRes)) }, + selected = currentSortType == sortType + ) + } + } + } +} + +@Composable +private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { + // 添加交互状态 + 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 = "menuItemScale" + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .scale(scale) + .clickable( + interactionSource = interactionSource, + indication = null + ) { menuItem.onClick() } + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.size(48.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) { + Box( + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = menuItem.icon, + contentDescription = stringResource(menuItem.titleRes), + modifier = Modifier.size(24.dp) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(menuItem.titleRes), + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.Center, + maxLines = 2 + ) + } +} + +@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.02f + 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.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + } + ), + elevation = CardDefaults.cardElevation( + defaultElevation = if (isSelected) 6.dp else 2.dp + ) + ) { + 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.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + 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.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + textAlign = TextAlign.Center, + maxLines = 2, + fontSize = 10.sp + ) + + // 应用数量 + Text( + text = appCount.toString(), + style = MaterialTheme.typography.labelSmall, + color = if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) + }, + modifier = Modifier.padding(top = 1.dp), + fontSize = 9.sp + ) } } } @@ -643,15 +690,29 @@ private fun AppItem( app: SuperUserViewModel.AppInfo, isSelected: Boolean, onToggleSelection: () -> Unit, - onSwitchChange: (Boolean) -> Unit, onClick: () -> Unit, onLongClick: () -> Unit, viewModel: SuperUserViewModel ) { + // 添加交互状态 + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + // 优化的缩放动画 + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.98f else 1.0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessHigh + ), + label = "appItemScale" + ) + Card( colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), elevation = getCardElevation(), modifier = Modifier + .scale(scale) .pointerInput(Unit) { detectTapGestures( onLongPress = { onLongClick() }, @@ -699,45 +760,19 @@ private fun AppItem( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { if (app.allowSu) { - LabelItem(text = "ROOT") + LabelItem(text = stringResource(R.string.label_root)) } if (Natives.uidShouldUmount(app.uid)) { - LabelItem(text = "UNMOUNT") + LabelItem(text = stringResource(R.string.label_unmount)) } if (app.hasCustomProfile) { - LabelItem(text = "CUSTOM") + LabelItem(text = stringResource(R.string.label_custom)) } } } - if (!viewModel.showBatchActions) { - // 开关交互源 - val switchInteractionSource = remember { MutableInteractionSource() } - val isSwitchPressed by switchInteractionSource.collectIsPressedAsState() - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - AnimatedVisibility( - visible = isSwitchPressed, - enter = expandHorizontally() + fadeIn(), - exit = shrinkHorizontally() + fadeOut() - ) { - Text( - text = if (app.allowSu) stringResource(R.string.authorized) else stringResource(R.string.unauthorized), - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(end = 4.dp) - ) - } - - Switch( - checked = app.allowSu, - onCheckedChange = onSwitchChange, - interactionSource = switchInteractionSource, - ) - } - } else { + + if (viewModel.showBatchActions) { val checkboxInteractionSource = remember { MutableInteractionSource() } val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState() diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt index d2e59f92..49ba9eac 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt @@ -61,7 +61,6 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.sukisu.ultra.Natives import com.sukisu.ultra.R -import com.sukisu.ultra.ui.MainActivity import com.sukisu.ultra.ui.component.ImageEditorDialog import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.theme.CardConfig.cardElevation 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 0a978fd6..804ebdb1 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -304,9 +304,6 @@ 工具 清除 - Root 权限应用 - 自定义配置应用 - 默认配置应用 未找到应用 SELinux 已设置为启用状态 SELinux 已设置为禁用状态 @@ -353,7 +350,39 @@ 跟随系统 语言已更改,重启应用以应用更改 卡片暗度调节 - + + 错误代码 + 请查看日志 + 正在安装模块 %1$d/%2$d + %d 个模块安装失败 + 模块下载失败 + 内核刷写 + + 全部 + Root + 自定义 + 默认 + + 名称升序 + 名称降序 + 安装时间(新) + 安装时间(旧) + 大小降序 + 大小升序 + 使用频率 + + 此分类中没有应用 + + Root + 卸载 + 自定义 + + 取消授权 + 授权 + 卸载模块挂载 + 禁用卸载模块挂载 + 展开菜单 + 收起菜单 顶部 底部 滚动到顶部 @@ -363,11 +392,7 @@ 已选择 选择 禁用自定义卸载模块 - - 错误代码 - 请查看日志 - 正在安装模块 %1$d/%2$d - %d 个模块安装失败 - 模块下载失败 - 内核刷写 + + 菜单选项 + 排序方式 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index c99590c7..7814891c 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -306,9 +306,6 @@ Tools Removals - Applications with root privileges - Applications with customized configurations - Applications with unchanged defaults Application not found SELinux Enabled SELinux Disabled @@ -355,7 +352,39 @@ Follow System Language changed, restarting to apply changes Card Darkness Adjustment - + + error code + Please check the log + Module being installed %1$d/%2$d + %d Failed to install a new module + Module download failed + Kernel Flashing + + All + Root + Custom + Default + + Ascending order of name + Name descending + Installation time (new) + Installation time (old) + descending order of size + ascending order of size + frequency of use + + No application in this category + + Root + Unmount + Custom + + Delegation of authority + Authorizations + Unmounting Module Mounts + Disable uninstall module mounting + Expand menu + Put away the menu Top Bottom Scroll to top @@ -365,11 +394,7 @@ Selected option Disable custom uninstallation module - - error code - Please check the log - Module being installed %1$d/%2$d - %d Failed to install a new module - Module download failed - Kernel Flashing + + Menu Options + Sort by