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 540e418e..ff35254b 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 @@ -1,13 +1,15 @@ -package com.sukisu.ultra.ui.screen - import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectTapGestures +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.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -40,7 +42,6 @@ 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.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.util.ModuleModify import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel @@ -68,7 +69,8 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { LaunchedEffect(viewModel.search) { if (viewModel.search.isEmpty()) { - listState.scrollToItem(0) + // 取消自动滚动到顶部的行为 + // listState.scrollToItem(0) } } @@ -165,77 +167,309 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { }, snackbarHost = { SnackbarHost(snackBarHostState) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - bottomBar = { - AnimatedVisibility( - visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(), - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }) + floatingActionButton = { + // 侧边悬浮按钮集合 + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.End ) { - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - tonalElevation = cardElevation, - shadowElevation = cardElevation - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceEvenly + // 批量操作相关按钮 + // 只有在批量模式且有选中应用时才显示批量操作按钮 + if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + // 取消按钮 + val cancelInteractionSource = remember { MutableInteractionSource() } + val isCancelPressed by cancelInteractionSource.collectIsPressedAsState() + + FloatingActionButton( + onClick = { + viewModel.selectedApps = emptySet() + viewModel.showBatchActions = false + }, + modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp), + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + shape = CircleShape, + interactionSource = cancelInteractionSource, + elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) ) { - OutlinedButton( - onClick = { - viewModel.selectedApps = emptySet() - viewModel.showBatchActions = false - }, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center ) { Icon( imageVector = Icons.Filled.Close, - contentDescription = null, - modifier = Modifier.size(16.dp) + contentDescription = stringResource(android.R.string.cancel), + modifier = Modifier.size(24.dp) ) - Spacer(modifier = Modifier.width(6.dp)) - Text(stringResource(android.R.string.cancel)) + 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 + ) + } } + } - Button( - onClick = { - scope.launch { - viewModel.updateBatchPermissions(true) - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(6.dp)) - Text(stringResource(R.string.batch_authorization)) - } + // 取消授权按钮 + val unauthorizeInteractionSource = remember { MutableInteractionSource() } + val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState() - Button( - onClick = { - scope.launch { - viewModel.updateBatchPermissions(false) - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) + FloatingActionButton( + onClick = { + scope.launch { + viewModel.updateBatchPermissions(false) + } + }, + modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp), + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + shape = CircleShape, + interactionSource = unauthorizeInteractionSource, + elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center ) { Icon( imageVector = Icons.Filled.Block, - contentDescription = null, - modifier = Modifier.size(16.dp) + 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 = { + scope.launch { + viewModel.updateBatchPermissions(true) + } + }, + modifier = Modifier.size(if (isAuthorizePressed) 56.dp else 40.dp), + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + 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 + ) + } + } + } + + // 添加分隔 + 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), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + 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), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + shape = CircleShape, + interactionSource = umountDisableInteractionSource, + elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp) + ) { + 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), + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f), + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + 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) + } + } + }, + modifier = Modifier.size(if (isBottomBtnPressed) 56.dp else 40.dp), + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f), + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + 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 ) - Spacer(modifier = Modifier.width(6.dp)) - Text(stringResource(R.string.batch_cancel_authorization)) } } } @@ -256,7 +490,7 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( top = 8.dp, - bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp + bottom = 16.dp ) ) { // 获取分组后的应用列表 @@ -280,7 +514,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { val profile = Natives.getAppProfile(app.packageName, app.uid) val updatedProfile = profile.copy(allowSu = allowSu) if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() + // 不重新获取应用列表,避免滚动位置重置 + // viewModel.fetchAppList() + // 仅更新当前应用的配置 + viewModel.updateAppProfileLocally(app.packageName, updatedProfile) } } }, @@ -319,7 +556,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { val profile = Natives.getAppProfile(app.packageName, app.uid) val updatedProfile = profile.copy(allowSu = allowSu) if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() + // 不重新获取应用列表,避免滚动位置重置 + // viewModel.fetchAppList() + // 仅更新当前应用的配置 + viewModel.updateAppProfileLocally(app.packageName, updatedProfile) } } }, @@ -358,7 +598,10 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { val profile = Natives.getAppProfile(app.packageName, app.uid) val updatedProfile = profile.copy(allowSu = allowSu) if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() + // 不重新获取应用列表,避免滚动位置重置 + // viewModel.fetchAppList() + // 仅更新当前应用的配置 + viewModel.updateAppProfileLocally(app.packageName, updatedProfile) } } }, @@ -539,27 +782,73 @@ private fun AppItem( } if (!viewModel.showBatchActions) { - Switch( - checked = app.allowSu, - onCheckedChange = onSwitchChange, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - checkedTrackColor = MaterialTheme.colorScheme.primary, - checkedIconColor = MaterialTheme.colorScheme.primary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant + // 开关交互源 + 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, + color = if (app.allowSu) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, + modifier = Modifier.padding(end = 4.dp) + ) + } + + Switch( + checked = app.allowSu, + onCheckedChange = onSwitchChange, + interactionSource = switchInteractionSource, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.onPrimary, + checkedTrackColor = MaterialTheme.colorScheme.primary, + checkedIconColor = MaterialTheme.colorScheme.primary, + uncheckedThumbColor = MaterialTheme.colorScheme.outline, + uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, + uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant + ) ) - ) + } } else { - Checkbox( - checked = isSelected, - onCheckedChange = { onToggleSelection() }, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.primary, - uncheckedColor = MaterialTheme.colorScheme.outline + // 复选框交互源 + val checkboxInteractionSource = remember { MutableInteractionSource() } + val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + AnimatedVisibility( + visible = isCheckboxPressed, + enter = expandHorizontally() + fadeIn(), + exit = shrinkHorizontally() + fadeOut() + ) { + Text( + text = if (isSelected) stringResource(R.string.selected) else stringResource(R.string.select), + style = MaterialTheme.typography.labelMedium, + color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, + modifier = Modifier.padding(end = 4.dp) + ) + } + + Checkbox( + checked = isSelected, + onCheckedChange = { onToggleSelection() }, + interactionSource = checkboxInteractionSource, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary, + uncheckedColor = MaterialTheme.colorScheme.outline + ) ) - ) + } } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt index 0c89211d..2fcec85f 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt @@ -1,5 +1,6 @@ package com.sukisu.ultra.ui.screen +import LabelText import android.content.ClipData import android.content.ClipboardManager import android.widget.Toast 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 f1750b9e..df3a2e5b 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 @@ -139,6 +139,43 @@ class SuperUserViewModel : ViewModel() { fetchAppList() // 刷新列表以显示最新状态 } + // 批量更新权限和umount模块设置 + suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) { + selectedApps.forEach { packageName -> + val app = apps.find { it.packageName == packageName } + app?.let { + val profile = Natives.getAppProfile(packageName, it.uid) + val updatedProfile = profile.copy( + allowSu = allowSu, + umountModules = umountModules ?: profile.umountModules, + nonRootUseDefault = false + ) + if (Natives.setAppProfile(updatedProfile)) { + apps = apps.map { app -> + if (app.packageName == packageName) { + app.copy(profile = updatedProfile) + } else { + app + } + } + } + } + } + clearSelection() + showBatchActions = false // 批量操作完成后退出批量模式 + fetchAppList() // 刷新列表以显示最新状态 + } + + // 仅更新本地应用配置,避免重新获取整个列表导致滚动位置重置 + fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { + apps = apps.map { app -> + if (app.packageName == packageName) { + app.copy(profile = updatedProfile) + } else { + app + } + } + } suspend fun fetchAppList() { isRefreshing = true 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 312ca0ed..05cd0bc6 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -347,4 +347,14 @@ 跟随系统 语言已更改,重启应用以应用更改 卡片暗度调节 + + 顶部 + 底部 + 滚动到顶部 + 滚动到底部 + 已授权 + 未授权 + 已选择 + 选择 + 禁用自定义卸载模块 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 3d3f7ab6..9cbc19fc 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -351,4 +351,14 @@ Follow System Language changed, restarting to apply changes Card Darkness Adjustment + + Top + Bottom + Scroll to top + Scroll to the bottom + authorized + unauthorized + Selected + option + Disable custom uninstallation module