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.webui)
|
||||||
implementation(libs.mmrl.ui)
|
implementation(libs.mmrl.ui)
|
||||||
|
|
||||||
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加尝试卸载对话框
|
* 添加尝试卸载对话框
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user