manager: Modify the batch selection ui on the superuser page
- Add more convenient buttons for it
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -40,7 +42,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
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.util.ModuleModify
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
|
||||||
@@ -68,7 +69,8 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
LaunchedEffect(viewModel.search) {
|
LaunchedEffect(viewModel.search) {
|
||||||
if (viewModel.search.isEmpty()) {
|
if (viewModel.search.isEmpty()) {
|
||||||
listState.scrollToItem(0)
|
// 取消自动滚动到顶部的行为
|
||||||
|
// listState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,77 +167,309 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
bottomBar = {
|
floatingActionButton = {
|
||||||
AnimatedVisibility(
|
// 侧边悬浮按钮集合
|
||||||
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
|
Column(
|
||||||
enter = slideInVertically(initialOffsetY = { it }),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
exit = slideOutVertically(targetOffsetY = { it })
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
Surface(
|
// 批量操作相关按钮
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||||
tonalElevation = cardElevation,
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
shadowElevation = cardElevation
|
// 取消按钮
|
||||||
) {
|
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||||
Row(
|
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
FloatingActionButton(
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.selectedApps = emptySet()
|
viewModel.selectedApps = emptySet()
|
||||||
viewModel.showBatchActions = false
|
viewModel.showBatchActions = false
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||||
contentColor = MaterialTheme.colorScheme.primary
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
)
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = cancelInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(android.R.string.cancel),
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text(stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.updateBatchPermissions(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCancelPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
) {
|
) {
|
||||||
Icon(
|
Text(
|
||||||
imageVector = Icons.Filled.Check,
|
stringResource(android.R.string.cancel),
|
||||||
contentDescription = null,
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
modifier = Modifier.size(16.dp)
|
style = MaterialTheme.typography.labelMedium
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
}
|
||||||
Text(stringResource(R.string.batch_authorization))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
// 取消授权按钮
|
||||||
|
val unauthorizeInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.updateBatchPermissions(false)
|
viewModel.updateBatchPermissions(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(
|
modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp),
|
||||||
containerColor = MaterialTheme.colorScheme.error
|
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(
|
Icon(
|
||||||
imageVector = Icons.Filled.Block,
|
imageVector = Icons.Filled.Block,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(R.string.batch_cancel_authorization),
|
||||||
modifier = Modifier.size(16.dp)
|
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),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
top = 8.dp,
|
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 profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
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 profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
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 profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
viewModel.fetchAppList()
|
// 不重新获取应用列表,避免滚动位置重置
|
||||||
|
// viewModel.fetchAppList()
|
||||||
|
// 仅更新当前应用的配置
|
||||||
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -539,9 +782,31 @@ private fun AppItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!viewModel.showBatchActions) {
|
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,
|
||||||
|
color = if (app.allowSu) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
checked = app.allowSu,
|
checked = app.allowSu,
|
||||||
onCheckedChange = onSwitchChange,
|
onCheckedChange = onSwitchChange,
|
||||||
|
interactionSource = switchInteractionSource,
|
||||||
colors = SwitchDefaults.colors(
|
colors = SwitchDefaults.colors(
|
||||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
@@ -551,10 +816,33 @@ private fun AppItem(
|
|||||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 复选框交互源
|
||||||
|
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(
|
Checkbox(
|
||||||
checked = isSelected,
|
checked = isSelected,
|
||||||
onCheckedChange = { onToggleSelection() },
|
onCheckedChange = { onToggleSelection() },
|
||||||
|
interactionSource = checkboxInteractionSource,
|
||||||
colors = CheckboxDefaults.colors(
|
colors = CheckboxDefaults.colors(
|
||||||
checkedColor = MaterialTheme.colorScheme.primary,
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
@@ -564,6 +852,7 @@ private fun AppItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelText(label: String, backgroundColor: Color) {
|
fun LabelText(label: String, backgroundColor: Color) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import LabelText
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|||||||
@@ -139,6 +139,43 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
fetchAppList() // 刷新列表以显示最新状态
|
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() {
|
suspend fun fetchAppList() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
|
|||||||
@@ -347,4 +347,14 @@
|
|||||||
<string name="language_follow_system">跟随系统</string>
|
<string name="language_follow_system">跟随系统</string>
|
||||||
<string name="language_changed">语言已更改,重启应用以应用更改</string>
|
<string name="language_changed">语言已更改,重启应用以应用更改</string>
|
||||||
<string name="settings_card_dim">卡片暗度调节</string>
|
<string name="settings_card_dim">卡片暗度调节</string>
|
||||||
|
<!-- 超级用户相关 -->
|
||||||
|
<string name="scroll_to_top">顶部</string>
|
||||||
|
<string name="scroll_to_bottom">底部</string>
|
||||||
|
<string name="scroll_to_top_description">滚动到顶部</string>
|
||||||
|
<string name="scroll_to_bottom_description">滚动到底部</string>
|
||||||
|
<string name="authorized">已授权</string>
|
||||||
|
<string name="unauthorized">未授权</string>
|
||||||
|
<string name="selected">已选择</string>
|
||||||
|
<string name="select">选择</string>
|
||||||
|
<string name="profile_umount_modules_disable">禁用自定义卸载模块</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -351,4 +351,14 @@
|
|||||||
<string name="language_follow_system">Follow System</string>
|
<string name="language_follow_system">Follow System</string>
|
||||||
<string name="language_changed">Language changed, restarting to apply changes</string>
|
<string name="language_changed">Language changed, restarting to apply changes</string>
|
||||||
<string name="settings_card_dim">Card Darkness Adjustment</string>
|
<string name="settings_card_dim">Card Darkness Adjustment</string>
|
||||||
|
<!-- Super User Related -->
|
||||||
|
<string name="scroll_to_top">Top</string>
|
||||||
|
<string name="scroll_to_bottom">Bottom</string>
|
||||||
|
<string name="scroll_to_top_description">Scroll to top</string>
|
||||||
|
<string name="scroll_to_bottom_description">Scroll to the bottom</string>
|
||||||
|
<string name="authorized">authorized</string>
|
||||||
|
<string name="unauthorized">unauthorized</string>
|
||||||
|
<string name="selected">Selected</string>
|
||||||
|
<string name="select">option</string>
|
||||||
|
<string name="profile_umount_modules_disable">Disable custom uninstallation module</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user