manager: Adding optional additions to SUS paths applies functionality corresponding to the package name as well as categorization
This commit is contained in:
@@ -159,4 +159,6 @@ dependencies {
|
||||
implementation(libs.mmrl.webui)
|
||||
implementation(libs.mmrl.ui)
|
||||
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
}
|
||||
@@ -1,19 +1,34 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Apps
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -26,11 +41,16 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||
|
||||
/**
|
||||
* 添加路径对话框
|
||||
@@ -105,6 +125,287 @@ fun AddPathDialog(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷添加应用路径对话框
|
||||
*/
|
||||
@Composable
|
||||
fun AddAppPathDialog(
|
||||
showDialog: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (List<String>) -> Unit,
|
||||
isLoading: Boolean,
|
||||
apps: List<SuSFSManager.AppInfo> = emptyList(),
|
||||
onLoadApps: () -> Unit,
|
||||
existingSusPaths: Set<String> = emptySet()
|
||||
) {
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
var selectedApps by remember { mutableStateOf(setOf<SuSFSManager.AppInfo>()) }
|
||||
|
||||
// 获取已添加的包名
|
||||
val addedPackageNames = remember(existingSusPaths) {
|
||||
existingSusPaths.mapNotNull { path ->
|
||||
val regex = Regex(".*/Android/data/([^/]+)/?.*")
|
||||
regex.find(path)?.groupValues?.get(1)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
// 过滤掉已添加的应用
|
||||
val availableApps = remember(apps, addedPackageNames) {
|
||||
apps.filter { app ->
|
||||
!addedPackageNames.contains(app.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
val filteredApps = remember(availableApps, searchText) {
|
||||
if (searchText.isBlank()) {
|
||||
availableApps
|
||||
} else {
|
||||
availableApps.filter { app ->
|
||||
app.appName.contains(searchText, ignoreCase = true) ||
|
||||
app.packageName.contains(searchText, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showDialog) {
|
||||
if (showDialog && apps.isEmpty()) {
|
||||
onLoadApps()
|
||||
}
|
||||
// 当对话框显示时清空选择
|
||||
if (showDialog) {
|
||||
selectedApps = setOf()
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_add_app_path),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
label = { Text(stringResource(R.string.search_apps)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
|
||||
// 显示统计信息
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
if (selectedApps.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.selected_apps_count, selectedApps.size),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
if (addedPackageNames.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.already_added_apps_count, addedPackageNames.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredApps.isEmpty()) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = if (availableApps.isEmpty()) {
|
||||
stringResource(R.string.all_apps_already_added)
|
||||
} else {
|
||||
stringResource(R.string.no_apps_found)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.height(300.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(filteredApps) { app ->
|
||||
val isSelected = selectedApps.contains(app)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
),
|
||||
onClick = {
|
||||
selectedApps = if (isSelected) {
|
||||
selectedApps - app
|
||||
} else {
|
||||
selectedApps + app
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 应用图标
|
||||
AppIcon(
|
||||
packageName = app.packageName,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = app.appName,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = app.packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 选择指示器
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RadioButtonUnchecked,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (selectedApps.isNotEmpty()) {
|
||||
onConfirm(selectedApps.map { it.packageName })
|
||||
}
|
||||
selectedApps = setOf()
|
||||
searchText = ""
|
||||
},
|
||||
enabled = selectedApps.isNotEmpty() && !isLoading,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.add)
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
selectedApps = setOf()
|
||||
searchText = ""
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 应用图标组件
|
||||
*/
|
||||
@Composable
|
||||
fun AppIcon(
|
||||
packageName: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var appIcon by remember(packageName) { mutableStateOf<Drawable?>(null) }
|
||||
|
||||
LaunchedEffect(packageName) {
|
||||
try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
appIcon = packageManager.getApplicationIcon(applicationInfo)
|
||||
} catch (_: Exception) {
|
||||
appIcon = null
|
||||
}
|
||||
}
|
||||
|
||||
if (appIcon != null) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(appIcon),
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
)
|
||||
} else {
|
||||
// 默认图标
|
||||
Icon(
|
||||
imageVector = Icons.Default.Apps,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加尝试卸载对话框
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Apps
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
@@ -31,6 +32,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -39,10 +41,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.AppPathGroupCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.PathItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.SectionHeader
|
||||
import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8
|
||||
@@ -55,22 +59,77 @@ fun SusPathsContent(
|
||||
susPaths: Set<String>,
|
||||
isLoading: Boolean,
|
||||
onAddPath: () -> Unit,
|
||||
onAddAppPath: () -> Unit,
|
||||
onRemovePath: (String) -> Unit,
|
||||
onEditPath: ((String) -> Unit)? = null
|
||||
) {
|
||||
val (appPathGroups, otherPaths) = remember(susPaths) {
|
||||
val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*")
|
||||
val appPathMap = mutableMapOf<String, MutableList<String>>()
|
||||
val others = mutableListOf<String>()
|
||||
|
||||
susPaths.forEach { path ->
|
||||
val matchResult = appPathRegex.find(path)
|
||||
if (matchResult != null) {
|
||||
val packageName = matchResult.groupValues[1]
|
||||
appPathMap.getOrPut(packageName) { mutableListOf() }.add(path)
|
||||
} else {
|
||||
others.add(path)
|
||||
}
|
||||
}
|
||||
|
||||
val sortedAppGroups = appPathMap.toList()
|
||||
.sortedBy { it.first }
|
||||
.map { (packageName, paths) -> packageName to paths.sorted() }
|
||||
|
||||
Pair(sortedAppGroups, others.sorted())
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (susPaths.isEmpty()) {
|
||||
// 应用路径分组
|
||||
if (appPathGroups.isNotEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_paths_configured)
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.app_paths_section),
|
||||
subtitle = null,
|
||||
icon = Icons.Default.Apps,
|
||||
count = appPathGroups.size
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(susPaths.toList()) { path ->
|
||||
|
||||
items(appPathGroups) { (packageName, paths) ->
|
||||
AppPathGroupCard(
|
||||
packageName = packageName,
|
||||
paths = paths,
|
||||
onDeleteGroup = {
|
||||
paths.forEach { path -> onRemovePath(path) }
|
||||
},
|
||||
onEditGroup = if (onEditPath != null) {
|
||||
{
|
||||
onEditPath(paths.first())
|
||||
}
|
||||
} else null,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 其他路径
|
||||
if (otherPaths.isNotEmpty()) {
|
||||
item {
|
||||
SectionHeader(
|
||||
title = stringResource(R.string.other_paths_section),
|
||||
subtitle = null,
|
||||
icon = Icons.Default.Folder,
|
||||
count = otherPaths.size
|
||||
)
|
||||
}
|
||||
|
||||
items(otherPaths) { path ->
|
||||
PathItemCard(
|
||||
path = path,
|
||||
icon = Icons.Default.Folder,
|
||||
@@ -81,7 +140,14 @@ fun SusPathsContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
if (susPaths.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_paths_configured)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -103,7 +169,23 @@ fun SusPathsContent(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
Text(text = stringResource(R.string.add_custom_path))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onAddAppPath,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Apps,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add_app_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +240,6 @@ fun SusMountsContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -204,8 +285,7 @@ fun TryUmountContent(
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (isSusVersion_1_5_8()) {
|
||||
@@ -289,7 +369,6 @@ fun TryUmountContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -359,7 +438,6 @@ fun KstatConfigContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 说明卡片
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -402,7 +480,6 @@ fun KstatConfigContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 静态Kstat配置列表
|
||||
if (kstatConfigs.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
@@ -421,7 +498,6 @@ fun KstatConfigContent(
|
||||
}
|
||||
}
|
||||
|
||||
// Add Kstat路径列表
|
||||
if (addKstatPaths.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
@@ -442,7 +518,6 @@ fun KstatConfigContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态显示
|
||||
if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
@@ -451,7 +526,6 @@ fun KstatConfigContent(
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -515,7 +589,6 @@ fun PathSettingsContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Android Data路径设置
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -550,7 +623,6 @@ fun PathSettingsContent(
|
||||
}
|
||||
}
|
||||
|
||||
// SD卡路径设置
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -599,7 +671,6 @@ fun EnabledFeaturesContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 说明卡片
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -68,6 +68,7 @@ import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.AddAppPathDialog
|
||||
import com.sukisu.ultra.ui.component.AddKstatStaticallyDialog
|
||||
import com.sukisu.ultra.ui.component.AddPathDialog
|
||||
import com.sukisu.ultra.ui.component.AddTryUmountDialog
|
||||
@@ -158,8 +159,12 @@ fun SuSFSConfigScreen(
|
||||
var enabledFeatures by remember { mutableStateOf(emptyList<SuSFSManager.EnabledFeature>()) }
|
||||
var isLoadingFeatures by remember { mutableStateOf(false) }
|
||||
|
||||
// 应用列表相关状态
|
||||
var installedApps by remember { mutableStateOf(emptyList<SuSFSManager.AppInfo>()) }
|
||||
|
||||
// 对话框状态
|
||||
var showAddPathDialog by remember { mutableStateOf(false) }
|
||||
var showAddAppPathDialog by remember { mutableStateOf(false) }
|
||||
var showAddMountDialog by remember { mutableStateOf(false) }
|
||||
var showAddUmountDialog by remember { mutableStateOf(false) }
|
||||
var showRunUmountDialog by remember { mutableStateOf(false) }
|
||||
@@ -263,6 +268,13 @@ fun SuSFSConfigScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 加载应用列表
|
||||
fun loadInstalledApps() {
|
||||
coroutineScope.launch {
|
||||
installedApps = SuSFSManager.getInstalledApps()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载槽位信息
|
||||
fun loadSlotInfo() {
|
||||
coroutineScope.launch {
|
||||
@@ -537,6 +549,31 @@ fun SuSFSConfigScreen(
|
||||
initialValue = editingPath ?: ""
|
||||
)
|
||||
|
||||
AddAppPathDialog(
|
||||
showDialog = showAddAppPathDialog,
|
||||
onDismiss = { showAddAppPathDialog = false },
|
||||
onConfirm = { packageNames ->
|
||||
coroutineScope.launch {
|
||||
isLoading = true
|
||||
var successCount = 0
|
||||
packageNames.forEach { packageName ->
|
||||
if (SuSFSManager.addAppPaths(context, packageName)) {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
if (successCount > 0) {
|
||||
susPaths = SuSFSManager.getSusPaths(context)
|
||||
}
|
||||
isLoading = false
|
||||
showAddAppPathDialog = false
|
||||
}
|
||||
},
|
||||
isLoading = isLoading,
|
||||
apps = installedApps,
|
||||
onLoadApps = { loadInstalledApps() },
|
||||
existingSusPaths = susPaths
|
||||
)
|
||||
|
||||
AddPathDialog(
|
||||
showDialog = showAddMountDialog,
|
||||
onDismiss = {
|
||||
@@ -1123,6 +1160,7 @@ fun SuSFSConfigScreen(
|
||||
susPaths = susPaths,
|
||||
isLoading = isLoading,
|
||||
onAddPath = { showAddPathDialog = true },
|
||||
onAddAppPath = { showAddAppPathDialog = true },
|
||||
onRemovePath = { path ->
|
||||
coroutineScope.launch {
|
||||
isLoading = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sukisu.ultra.ui.screen.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -33,6 +35,7 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -49,6 +52,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.AppIcon
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -633,3 +637,179 @@ fun SusMountHidingControlCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用路径分组卡片
|
||||
*/
|
||||
@Composable
|
||||
fun AppPathGroupCard(
|
||||
packageName: String,
|
||||
paths: List<String>,
|
||||
onDeleteGroup: () -> Unit,
|
||||
onEditGroup: (() -> Unit)? = null,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var appName by remember(packageName) { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(packageName) {
|
||||
try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
appName = packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
} catch (_: Exception) {
|
||||
appName = packageName
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 应用图标
|
||||
AppIcon(
|
||||
packageName = packageName,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = appName.ifEmpty { packageName },
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (appName.isNotEmpty() && appName != packageName) {
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (onEditGroup != null) {
|
||||
IconButton(
|
||||
onClick = onEditGroup,
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = stringResource(R.string.edit),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onDeleteGroup,
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示所有路径
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
paths.forEach { path ->
|
||||
Text(
|
||||
text = path,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
RoundedCornerShape(6.dp)
|
||||
)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
if (path != paths.last()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组标题组件
|
||||
*/
|
||||
@Composable
|
||||
fun SectionHeader(
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
icon: ImageVector,
|
||||
count: Int
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.sukisu.ultra.ui.util
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.widget.Toast
|
||||
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
||||
import com.sukisu.ultra.Natives
|
||||
@@ -16,6 +18,7 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -62,6 +65,15 @@ object SuSFSManager {
|
||||
val canConfigure: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* 应用信息数据类
|
||||
*/
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val isSystemApp: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* 备份数据类
|
||||
*/
|
||||
@@ -349,6 +361,120 @@ object SuSFSManager {
|
||||
fun getSdcardPath(context: Context): String =
|
||||
getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard"
|
||||
|
||||
/**
|
||||
* 获取已安装的应用列表
|
||||
*/
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
suspend fun getInstalledApps(): List<AppInfo> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val pm = context.packageManager
|
||||
val allApps = mutableMapOf<String, AppInfo>()
|
||||
|
||||
// 从SuperUser中获取应用
|
||||
SuperUserViewModel.apps.forEach { superUserApp ->
|
||||
try {
|
||||
val isSystemApp = superUserApp.packageInfo.applicationInfo?.let {
|
||||
(it.flags and ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
} ?: false
|
||||
if (!isSystemApp) {
|
||||
allApps[superUserApp.packageName] = AppInfo(
|
||||
packageName = superUserApp.packageName,
|
||||
appName = superUserApp.label,
|
||||
isSystemApp = false
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
// 从PackageManager获取所有应用
|
||||
val installedPackages = pm.getInstalledPackages(PackageManager.GET_META_DATA)
|
||||
installedPackages.forEach { packageInfo ->
|
||||
val packageName = packageInfo.packageName
|
||||
val isSystemApp = packageInfo.applicationInfo?.let { (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 }
|
||||
|
||||
// 只处理非系统应用且不在SuperUser列表中的应用
|
||||
if (!isSystemApp!! && !allApps.containsKey(packageName)) {
|
||||
try {
|
||||
val appName = packageInfo.applicationInfo?.loadLabel(pm).toString()
|
||||
allApps[packageName] = AppInfo(
|
||||
packageName = packageName,
|
||||
appName = appName,
|
||||
isSystemApp = false
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
allApps[packageName] = AppInfo(
|
||||
packageName = packageName,
|
||||
appName = packageName,
|
||||
isSystemApp = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加可能遗漏的当前应用
|
||||
val currentPackageName = context.packageName
|
||||
if (!allApps.containsKey(currentPackageName)) {
|
||||
try {
|
||||
val currentAppInfo = pm.getPackageInfo(currentPackageName, 0)
|
||||
val currentAppName = currentAppInfo.applicationInfo?.loadLabel(pm).toString()
|
||||
allApps[currentPackageName] = AppInfo(
|
||||
packageName = currentPackageName,
|
||||
appName = currentAppName,
|
||||
isSystemApp = false
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
allApps[currentPackageName] = AppInfo(
|
||||
packageName = currentPackageName,
|
||||
appName = "com.sukisu.ultra",
|
||||
isSystemApp = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
allApps.values.sortedBy { it.appName }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 快捷添加应用路径
|
||||
*/
|
||||
suspend fun addAppPaths(context: Context, packageName: String): Boolean {
|
||||
val androidDataPath = getAndroidDataPath(context)
|
||||
getSdcardPath(context)
|
||||
|
||||
val path1 = "$androidDataPath/$packageName"
|
||||
val path2 = "/data/media/0/Android/data/$packageName"
|
||||
|
||||
var successCount = 0
|
||||
var totalCount = 0
|
||||
|
||||
// 添加第一个路径
|
||||
totalCount++
|
||||
if (addSusPath(context, path1)) {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 添加第二个路径
|
||||
totalCount++
|
||||
if (addSusPath(context, path2)) {
|
||||
successCount++
|
||||
}
|
||||
|
||||
val success = successCount > 0
|
||||
if (success) {
|
||||
""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
// 获取所有配置的Map
|
||||
private fun getAllConfigurations(context: Context): Map<String, Any> {
|
||||
return mapOf(
|
||||
|
||||
@@ -557,4 +557,13 @@
|
||||
<string name="umount_zygote_iso_service_description">启用此选项将在系统启动时卸载Zygote隔离服务挂载点</string>
|
||||
<string name="umount_zygote_iso_service_enabled">Zygote隔离服务卸载已启用</string>
|
||||
<string name="umount_zygote_iso_service_disabled">Zygote隔离服务卸载已禁用</string>
|
||||
<string name="app_paths_section">应用路径</string>
|
||||
<string name="other_paths_section">其他路径</string>
|
||||
<string name="add_custom_path">其他</string>
|
||||
<string name="add_app_path">应用</string>
|
||||
<string name="susfs_add_app_path">添加应用路径</string>
|
||||
<string name="search_apps">搜索应用</string>
|
||||
<string name="selected_apps_count">%1$d 个已选应用</string>
|
||||
<string name="already_added_apps_count">%1$d 个已添加应用</string>
|
||||
<string name="all_apps_already_added">所有应用均已添加</string>
|
||||
</resources>
|
||||
|
||||
@@ -559,4 +559,13 @@
|
||||
<string name="umount_zygote_iso_service_description">Enable this option to unmount Zygote isolation service mount points at system startup</string>
|
||||
<string name="umount_zygote_iso_service_enabled">Zygote isolation service unmount enabled</string>
|
||||
<string name="umount_zygote_iso_service_disabled">Zygote isolation service unmount disabled</string>
|
||||
<string name="app_paths_section">Application Path</string>
|
||||
<string name="other_paths_section">Other paths</string>
|
||||
<string name="add_custom_path">Other</string>
|
||||
<string name="add_app_path">App</string>
|
||||
<string name="susfs_add_app_path">Add App Path</string>
|
||||
<string name="search_apps">Search Apps</string>
|
||||
<string name="selected_apps_count">%1$d apps selected</string>
|
||||
<string name="already_added_apps_count">%1$d apps already added</string>
|
||||
<string name="all_apps_already_added">All apps have been added</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[versions]
|
||||
accompanist-drawablepainter = "0.37.3"
|
||||
agp = "8.11.0"
|
||||
gson = "2.11.0"
|
||||
kotlin = "2.1.20"
|
||||
@@ -37,6 +38,7 @@ lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version.ref = "apksign
|
||||
lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" }
|
||||
|
||||
[libraries]
|
||||
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
|
||||
|
||||
androidx-foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
|
||||
Reference in New Issue
Block a user