manager: Adding optional additions to SUS paths applies functionality corresponding to the package name as well as categorization

This commit is contained in:
ShirkNeko
2025-07-01 17:29:45 +08:00
parent be14da387e
commit 2278fe49d2
9 changed files with 757 additions and 19 deletions

View File

@@ -159,4 +159,6 @@ dependencies {
implementation(libs.mmrl.webui) implementation(libs.mmrl.webui)
implementation(libs.mmrl.ui) implementation(libs.mmrl.ui)
implementation(libs.accompanist.drawablepainter)
} }

View File

@@ -1,19 +1,34 @@
package com.sukisu.ultra.ui.component 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -26,11 +41,16 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.sukisu.ultra.R 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
)
}
}
/** /**
* 添加尝试卸载对话框 * 添加尝试卸载对话框
*/ */

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add 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.Folder
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Security 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.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -39,10 +41,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard 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.EmptyStateCard
import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard
import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard
import com.sukisu.ultra.ui.screen.extensions.PathItemCard 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.screen.extensions.SusMountHidingControlCard
import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8 import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8
@@ -55,22 +59,77 @@ fun SusPathsContent(
susPaths: Set<String>, susPaths: Set<String>,
isLoading: Boolean, isLoading: Boolean,
onAddPath: () -> Unit, onAddPath: () -> Unit,
onAddAppPath: () -> Unit,
onRemovePath: (String) -> Unit, onRemovePath: (String) -> Unit,
onEditPath: ((String) -> Unit)? = null 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()) { Box(modifier = Modifier.fillMaxSize()) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (susPaths.isEmpty()) { // 应用路径分组
if (appPathGroups.isNotEmpty()) {
item { item {
EmptyStateCard( SectionHeader(
message = stringResource(R.string.susfs_no_paths_configured) 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( PathItemCard(
path = path, path = path,
icon = Icons.Default.Folder, icon = Icons.Default.Folder,
@@ -81,7 +140,14 @@ fun SusPathsContent(
} }
} }
// 添加普通长按钮 if (susPaths.isEmpty()) {
item {
EmptyStateCard(
message = stringResource(R.string.susfs_no_paths_configured)
)
}
}
item { item {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -103,7 +169,23 @@ fun SusPathsContent(
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
Spacer(modifier = Modifier.width(8.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 { item {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -204,8 +285,7 @@ fun TryUmountContent(
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
if (isSusVersion_1_5_8()) { if (isSusVersion_1_5_8()) {
@@ -289,7 +369,6 @@ fun TryUmountContent(
} }
} }
// 添加普通长按钮
item { item {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -359,7 +438,6 @@ fun KstatConfigContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// 说明卡片
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -402,7 +480,6 @@ fun KstatConfigContent(
} }
} }
// 静态Kstat配置列表
if (kstatConfigs.isNotEmpty()) { if (kstatConfigs.isNotEmpty()) {
item { item {
Text( Text(
@@ -421,7 +498,6 @@ fun KstatConfigContent(
} }
} }
// Add Kstat路径列表
if (addKstatPaths.isNotEmpty()) { if (addKstatPaths.isNotEmpty()) {
item { item {
Text( Text(
@@ -442,7 +518,6 @@ fun KstatConfigContent(
} }
} }
// 空状态显示
if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) { if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) {
item { item {
EmptyStateCard( EmptyStateCard(
@@ -451,7 +526,6 @@ fun KstatConfigContent(
} }
} }
// 添加普通长按钮
item { item {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -515,7 +589,6 @@ fun PathSettingsContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Android Data路径设置
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -550,7 +623,6 @@ fun PathSettingsContent(
} }
} }
// SD卡路径设置
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -599,7 +671,6 @@ fun EnabledFeaturesContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// 说明卡片
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View File

@@ -68,6 +68,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R 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.AddKstatStaticallyDialog
import com.sukisu.ultra.ui.component.AddPathDialog import com.sukisu.ultra.ui.component.AddPathDialog
import com.sukisu.ultra.ui.component.AddTryUmountDialog import com.sukisu.ultra.ui.component.AddTryUmountDialog
@@ -158,8 +159,12 @@ fun SuSFSConfigScreen(
var enabledFeatures by remember { mutableStateOf(emptyList<SuSFSManager.EnabledFeature>()) } var enabledFeatures by remember { mutableStateOf(emptyList<SuSFSManager.EnabledFeature>()) }
var isLoadingFeatures by remember { mutableStateOf(false) } var isLoadingFeatures by remember { mutableStateOf(false) }
// 应用列表相关状态
var installedApps by remember { mutableStateOf(emptyList<SuSFSManager.AppInfo>()) }
// 对话框状态 // 对话框状态
var showAddPathDialog by remember { mutableStateOf(false) } var showAddPathDialog by remember { mutableStateOf(false) }
var showAddAppPathDialog by remember { mutableStateOf(false) }
var showAddMountDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) }
var showAddUmountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) }
var showRunUmountDialog 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() { fun loadSlotInfo() {
coroutineScope.launch { coroutineScope.launch {
@@ -537,6 +549,31 @@ fun SuSFSConfigScreen(
initialValue = editingPath ?: "" 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( AddPathDialog(
showDialog = showAddMountDialog, showDialog = showAddMountDialog,
onDismiss = { onDismiss = {
@@ -1123,6 +1160,7 @@ fun SuSFSConfigScreen(
susPaths = susPaths, susPaths = susPaths,
isLoading = isLoading, isLoading = isLoading,
onAddPath = { showAddPathDialog = true }, onAddPath = { showAddPathDialog = true },
onAddAppPath = { showAddAppPathDialog = true },
onRemovePath = { path -> onRemovePath = { path ->
coroutineScope.launch { coroutineScope.launch {
isLoading = true isLoading = true

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.screen.extensions package com.sukisu.ultra.ui.screen.extensions
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -33,6 +35,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.AppIcon
import com.sukisu.ultra.ui.util.SuSFSManager import com.sukisu.ultra.ui.util.SuSFSManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -632,4 +636,180 @@ 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
)
}
}
}
} }

View File

@@ -3,6 +3,8 @@ package com.sukisu.ultra.ui.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.widget.Toast import android.widget.Toast
import com.dergoogler.mmrl.platform.Platform.Companion.context import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
@@ -16,6 +18,7 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import androidx.core.content.edit import androidx.core.content.edit
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -62,6 +65,15 @@ object SuSFSManager {
val canConfigure: Boolean = false 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 = fun getSdcardPath(context: Context): String =
getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard" 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 // 获取所有配置的Map
private fun getAllConfigurations(context: Context): Map<String, Any> { private fun getAllConfigurations(context: Context): Map<String, Any> {
return mapOf( return mapOf(

View File

@@ -557,4 +557,13 @@
<string name="umount_zygote_iso_service_description">启用此选项将在系统启动时卸载Zygote隔离服务挂载点</string> <string name="umount_zygote_iso_service_description">启用此选项将在系统启动时卸载Zygote隔离服务挂载点</string>
<string name="umount_zygote_iso_service_enabled">Zygote隔离服务卸载已启用</string> <string name="umount_zygote_iso_service_enabled">Zygote隔离服务卸载已启用</string>
<string name="umount_zygote_iso_service_disabled">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> </resources>

View File

@@ -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_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_enabled">Zygote isolation service unmount enabled</string>
<string name="umount_zygote_iso_service_disabled">Zygote isolation service unmount disabled</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> </resources>

View File

@@ -1,4 +1,5 @@
[versions] [versions]
accompanist-drawablepainter = "0.37.3"
agp = "8.11.0" agp = "8.11.0"
gson = "2.11.0" gson = "2.11.0"
kotlin = "2.1.20" 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" } lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" }
[libraries] [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-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
androidx-foundation = { module = "androidx.compose.foundation:foundation" } androidx-foundation = { module = "androidx.compose.foundation:foundation" }