manager: Add configuration for SuSFS logging feature, support enable/disable logging
This commit is contained in:
@@ -23,16 +23,11 @@ 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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.AutoMode
|
import androidx.compose.material.icons.filled.AutoMode
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Folder
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.RestoreFromTrash
|
import androidx.compose.material.icons.filled.RestoreFromTrash
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Storage
|
|
||||||
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
|
||||||
@@ -43,7 +38,6 @@ 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.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -82,6 +76,11 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||||
|
import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard
|
||||||
|
import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard
|
||||||
|
import com.sukisu.ultra.ui.screen.extensions.SusPathsContent
|
||||||
|
import com.sukisu.ultra.ui.screen.extensions.SusMountsContent
|
||||||
|
import com.sukisu.ultra.ui.screen.extensions.TryUmountContent
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,12 +150,10 @@ fun SuSFSConfigScreen(
|
|||||||
|
|
||||||
val allTabs = SuSFSTab.getAllTabs()
|
val allTabs = SuSFSTab.getAllTabs()
|
||||||
|
|
||||||
// 实时判断是否可以启用开机自启动
|
// 实时判断是否可以启用开机自启动 - 修改逻辑
|
||||||
val canEnableAutoStart by remember {
|
val canEnableAutoStart by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(unameValue.trim().isNotBlank() && unameValue.trim() != "default") ||
|
SuSFSManager.hasConfigurationForAutoStart(context)
|
||||||
(buildTimeValue.trim().isNotBlank() && buildTimeValue.trim() != "default") ||
|
|
||||||
susPaths.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +187,7 @@ fun SuSFSConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当输入值变化时,自动调整开机自启动状态
|
// 当配置变化时,自动调整开机自启动状态
|
||||||
LaunchedEffect(canEnableAutoStart) {
|
LaunchedEffect(canEnableAutoStart) {
|
||||||
if (!canEnableAutoStart && autoStartEnabled) {
|
if (!canEnableAutoStart && autoStartEnabled) {
|
||||||
autoStartEnabled = false
|
autoStartEnabled = false
|
||||||
@@ -742,7 +739,7 @@ fun SuSFSConfigScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
|
stringResource(R.string.susfs_reset_to_default),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -765,7 +762,7 @@ fun SuSFSConfigScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
|
stringResource(R.string.susfs_reset_paths_title),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -788,7 +785,7 @@ fun SuSFSConfigScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
|
stringResource(R.string.susfs_reset_mounts_title),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -811,7 +808,7 @@ fun SuSFSConfigScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
|
stringResource(R.string.susfs_reset_umounts_title),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -843,7 +840,7 @@ fun SuSFSConfigScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
|
stringResource(R.string.susfs_path_settings),
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1011,8 @@ fun SuSFSConfigScreen(
|
|||||||
}
|
}
|
||||||
SuSFSTab.ENABLED_FEATURES -> {
|
SuSFSTab.ENABLED_FEATURES -> {
|
||||||
EnabledFeaturesContent(
|
EnabledFeaturesContent(
|
||||||
enabledFeatures = enabledFeatures
|
enabledFeatures = enabledFeatures,
|
||||||
|
onRefresh = { loadEnabledFeatures() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1182,215 +1180,6 @@ private fun BasicSettingsContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* SUS路径内容组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun SusPathsContent(
|
|
||||||
susPaths: Set<String>,
|
|
||||||
isLoading: Boolean,
|
|
||||||
onAddPath: () -> Unit,
|
|
||||||
onRemovePath: (String) -> Unit
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
UnifiedButtonRow(
|
|
||||||
primaryButton = {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = onAddPath,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButtons = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.susfs_sus_paths_management),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (susPaths.isEmpty()) {
|
|
||||||
EmptyStateCard(
|
|
||||||
message = stringResource(R.string.susfs_no_paths_configured)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
items(susPaths.toList()) { path ->
|
|
||||||
PathItemCard(
|
|
||||||
path = path,
|
|
||||||
icon = Icons.Default.Folder,
|
|
||||||
onDelete = { onRemovePath(path) },
|
|
||||||
isLoading = isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SUS挂载内容组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun SusMountsContent(
|
|
||||||
susMounts: Set<String>,
|
|
||||||
isLoading: Boolean,
|
|
||||||
onAddMount: () -> Unit,
|
|
||||||
onRemoveMount: (String) -> Unit
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
UnifiedButtonRow(
|
|
||||||
primaryButton = {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = onAddMount,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButtons = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.susfs_sus_mounts_management),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (susMounts.isEmpty()) {
|
|
||||||
EmptyStateCard(
|
|
||||||
message = stringResource(R.string.susfs_no_mounts_configured)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
items(susMounts.toList()) { mount ->
|
|
||||||
PathItemCard(
|
|
||||||
path = mount,
|
|
||||||
icon = Icons.Default.Storage,
|
|
||||||
onDelete = { onRemoveMount(mount) },
|
|
||||||
isLoading = isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 尝试卸载内容组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun TryUmountContent(
|
|
||||||
tryUmounts: Set<String>,
|
|
||||||
isLoading: Boolean,
|
|
||||||
onAddUmount: () -> Unit,
|
|
||||||
onRunUmount: () -> Unit,
|
|
||||||
onRemoveUmount: (String) -> Unit
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
UnifiedButtonRow(
|
|
||||||
primaryButton = {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = onAddUmount,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButtons = {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.susfs_try_umount_management),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
if (tryUmounts.isNotEmpty()) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = onRunUmount,
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondary
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (tryUmounts.isEmpty()) {
|
|
||||||
EmptyStateCard(
|
|
||||||
message = stringResource(R.string.susfs_no_umounts_configured)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
items(tryUmounts.toList()) { umountEntry ->
|
|
||||||
val parts = umountEntry.split("|")
|
|
||||||
val path = if (parts.isNotEmpty()) parts[0] else umountEntry
|
|
||||||
val mode = if (parts.size > 1) parts[1] else "0"
|
|
||||||
val modeText = if (mode == "0")
|
|
||||||
stringResource(R.string.susfs_umount_mode_normal_short)
|
|
||||||
else
|
|
||||||
stringResource(R.string.susfs_umount_mode_detach_short)
|
|
||||||
|
|
||||||
PathItemCard(
|
|
||||||
path = path,
|
|
||||||
icon = Icons.Default.Storage,
|
|
||||||
additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode),
|
|
||||||
onDelete = { onRemoveUmount(umountEntry) },
|
|
||||||
isLoading = isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径设置内容组件
|
* 路径设置内容组件
|
||||||
*/
|
*/
|
||||||
@@ -1490,7 +1279,8 @@ private fun PathSettingsContent(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun EnabledFeaturesContent(
|
private fun EnabledFeaturesContent(
|
||||||
enabledFeatures: List<SuSFSManager.EnabledFeature>
|
enabledFeatures: List<SuSFSManager.EnabledFeature>,
|
||||||
|
onRefresh: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -1543,182 +1333,12 @@ private fun EnabledFeaturesContent(
|
|||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
items(enabledFeatures) { feature ->
|
items(enabledFeatures) { feature ->
|
||||||
FeatureStatusCard(feature = feature)
|
FeatureStatusCard(
|
||||||
}
|
feature = feature,
|
||||||
}
|
onRefresh = onRefresh
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一的按钮布局组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun UnifiedButtonRow(
|
|
||||||
primaryButton: @Composable () -> Unit,
|
|
||||||
secondaryButtons: @Composable () -> Unit = {},
|
|
||||||
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
secondaryButtons()
|
|
||||||
}
|
|
||||||
primaryButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 空状态显示组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun EmptyStateCard(
|
|
||||||
message: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路径项目卡片组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun PathItemCard(
|
|
||||||
path: String,
|
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
||||||
onDelete: () -> Unit,
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
additionalInfo: String? = null
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 1.dp),
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = path,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
)
|
||||||
if (additionalInfo != null) {
|
|
||||||
Text(
|
|
||||||
text = additionalInfo,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(
|
|
||||||
onClick = onDelete,
|
|
||||||
enabled = !isLoading,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启用功能状态卡片组件
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun FeatureStatusCard(
|
|
||||||
feature: SuSFSManager.EnabledFeature,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 1.dp),
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = feature.name,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(6.dp),
|
|
||||||
color = when {
|
|
||||||
feature.isEnabled -> MaterialTheme.colorScheme.primary
|
|
||||||
else -> Color.Gray
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = feature.statusText,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = when {
|
|
||||||
feature.isEnabled -> MaterialTheme.colorScheme.onPrimary
|
|
||||||
else -> Color.White
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen.extensions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
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.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Folder
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的按钮布局组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun UnifiedButtonRow(
|
||||||
|
primaryButton: @Composable () -> Unit,
|
||||||
|
secondaryButtons: @Composable () -> Unit = {},
|
||||||
|
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
secondaryButtons()
|
||||||
|
}
|
||||||
|
primaryButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空状态显示组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun EmptyStateCard(
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径项目卡片组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PathItemCard(
|
||||||
|
path: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
additionalInfo: String? = null
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 1.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = path,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
if (additionalInfo != null) {
|
||||||
|
Text(
|
||||||
|
text = additionalInfo,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onDelete,
|
||||||
|
enabled = !isLoading,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用功能状态卡片组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FeatureStatusCard(
|
||||||
|
feature: SuSFSManager.EnabledFeature,
|
||||||
|
onRefresh: (() -> Unit)? = null,
|
||||||
|
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// 日志配置对话框状态
|
||||||
|
var showLogConfigDialog by remember { mutableStateOf(false) }
|
||||||
|
var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) }
|
||||||
|
|
||||||
|
// 日志配置对话框
|
||||||
|
if (showLogConfigDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showLogConfigDialog = false },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_log_config_title),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_log_config_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_enable_log_label),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = logEnabled,
|
||||||
|
onCheckedChange = { logEnabled = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (SuSFSManager.setEnableLog(context, logEnabled)) {
|
||||||
|
onRefresh?.invoke()
|
||||||
|
}
|
||||||
|
showLogConfigDialog = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.susfs_apply))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
// 恢复原始状态
|
||||||
|
logEnabled = SuSFSManager.getEnableLogState(context)
|
||||||
|
showLogConfigDialog = false
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 1.dp)
|
||||||
|
.then(
|
||||||
|
if (feature.canConfigure) {
|
||||||
|
Modifier.clickable {
|
||||||
|
// 更新当前状态
|
||||||
|
logEnabled = SuSFSManager.getEnableLogState(context)
|
||||||
|
showLogConfigDialog = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = feature.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
if (feature.canConfigure) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_feature_configurable),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 状态标签
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = when {
|
||||||
|
feature.isEnabled -> MaterialTheme.colorScheme.primary
|
||||||
|
else -> Color.Gray
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = feature.statusText,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = when {
|
||||||
|
feature.isEnabled -> MaterialTheme.colorScheme.onPrimary
|
||||||
|
else -> Color.White
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUS路径内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SusPathsContent(
|
||||||
|
susPaths: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddPath: () -> Unit,
|
||||||
|
onRemovePath: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
UnifiedButtonRow(
|
||||||
|
primaryButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onAddPath,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButtons = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_sus_paths_management),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (susPaths.isEmpty()) {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_paths_configured)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
items(susPaths.toList()) { path ->
|
||||||
|
PathItemCard(
|
||||||
|
path = path,
|
||||||
|
icon = Icons.Default.Folder,
|
||||||
|
onDelete = { onRemovePath(path) },
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUS挂载内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SusMountsContent(
|
||||||
|
susMounts: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddMount: () -> Unit,
|
||||||
|
onRemoveMount: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
UnifiedButtonRow(
|
||||||
|
primaryButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onAddMount,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButtons = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_sus_mounts_management),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (susMounts.isEmpty()) {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_mounts_configured)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
items(susMounts.toList()) { mount ->
|
||||||
|
PathItemCard(
|
||||||
|
path = mount,
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
onDelete = { onRemoveMount(mount) },
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试卸载内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TryUmountContent(
|
||||||
|
tryUmounts: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddUmount: () -> Unit,
|
||||||
|
onRunUmount: () -> Unit,
|
||||||
|
onRemoveUmount: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
UnifiedButtonRow(
|
||||||
|
primaryButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onAddUmount,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
secondaryButtons = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_try_umount_management),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
if (tryUmounts.isNotEmpty()) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onRunUmount,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondary
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tryUmounts.isEmpty()) {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_umounts_configured)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
items(tryUmounts.toList()) { umountEntry ->
|
||||||
|
val parts = umountEntry.split("|")
|
||||||
|
val path = if (parts.isNotEmpty()) parts[0] else umountEntry
|
||||||
|
val mode = if (parts.size > 1) parts[1] else "0"
|
||||||
|
val modeText = if (mode == "0")
|
||||||
|
stringResource(R.string.susfs_umount_mode_normal_short)
|
||||||
|
else
|
||||||
|
stringResource(R.string.susfs_umount_mode_detach_short)
|
||||||
|
|
||||||
|
PathItemCard(
|
||||||
|
path = path,
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode),
|
||||||
|
onDelete = { onRemoveUmount(umountEntry) },
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,11 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
|
||||||
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -32,6 +31,7 @@ object SuSFSManager {
|
|||||||
private const val KEY_TRY_UMOUNTS = "try_umounts"
|
private const val KEY_TRY_UMOUNTS = "try_umounts"
|
||||||
private const val KEY_ANDROID_DATA_PATH = "android_data_path"
|
private const val KEY_ANDROID_DATA_PATH = "android_data_path"
|
||||||
private const val KEY_SDCARD_PATH = "sdcard_path"
|
private const val KEY_SDCARD_PATH = "sdcard_path"
|
||||||
|
private const val KEY_ENABLE_LOG = "enable_log"
|
||||||
private const val SUSFS_BINARY_BASE_NAME = "ksu_susfs"
|
private const val SUSFS_BINARY_BASE_NAME = "ksu_susfs"
|
||||||
private const val DEFAULT_UNAME = "default"
|
private const val DEFAULT_UNAME = "default"
|
||||||
private const val DEFAULT_BUILD_TIME = "default"
|
private const val DEFAULT_BUILD_TIME = "default"
|
||||||
@@ -63,7 +63,8 @@ object SuSFSManager {
|
|||||||
data class EnabledFeature(
|
data class EnabledFeature(
|
||||||
val name: String,
|
val name: String,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
val statusText: String = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled)
|
val statusText: String = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled),
|
||||||
|
val canConfigure: Boolean = false // 是否可配置(通过弹窗)
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,6 +195,23 @@ object SuSFSManager {
|
|||||||
return getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false)
|
return getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存日志启用状态
|
||||||
|
*/
|
||||||
|
fun saveEnableLogState(context: Context, enabled: Boolean) {
|
||||||
|
getPrefs(context).edit().apply {
|
||||||
|
putBoolean(KEY_ENABLE_LOG, enabled)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志启用状态
|
||||||
|
*/
|
||||||
|
fun getEnableLogState(context: Context): Boolean {
|
||||||
|
return getPrefs(context).getBoolean(KEY_ENABLE_LOG, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存SUS路径列表
|
* 保存SUS路径列表
|
||||||
*/
|
*/
|
||||||
@@ -343,6 +361,7 @@ object SuSFSManager {
|
|||||||
val tryUmounts = getTryUmounts(context)
|
val tryUmounts = getTryUmounts(context)
|
||||||
val androidDataPath = getAndroidDataPath(context)
|
val androidDataPath = getAndroidDataPath(context)
|
||||||
val sdcardPath = getSdcardPath(context)
|
val sdcardPath = getSdcardPath(context)
|
||||||
|
val enableLog = getEnableLogState(context)
|
||||||
val targetPath = getSuSFSTargetPath()
|
val targetPath = getSuSFSTargetPath()
|
||||||
|
|
||||||
val scriptContent = buildString {
|
val scriptContent = buildString {
|
||||||
@@ -359,6 +378,12 @@ object SuSFSManager {
|
|||||||
appendLine(" mkdir -p /data/adb/ksu/log")
|
appendLine(" mkdir -p /data/adb/ksu/log")
|
||||||
appendLine()
|
appendLine()
|
||||||
|
|
||||||
|
// 设置日志启用状态
|
||||||
|
appendLine(" # 设置日志启用状态")
|
||||||
|
val logValue = if (enableLog) 1 else 0
|
||||||
|
appendLine(" $targetPath enable_log $logValue")
|
||||||
|
appendLine(" echo \"\\$(date): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> /data/adb/ksu/log/susfs_startup.log")
|
||||||
|
|
||||||
// 设置Android Data路径
|
// 设置Android Data路径
|
||||||
if (androidDataPath != "/sdcard/Android/data") {
|
if (androidDataPath != "/sdcard/Android/data") {
|
||||||
appendLine(" # 设置Android Data路径")
|
appendLine(" # 设置Android Data路径")
|
||||||
@@ -495,6 +520,31 @@ object SuSFSManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用或禁用日志功能
|
||||||
|
*/
|
||||||
|
suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean {
|
||||||
|
val value = if (enabled) 1 else 0
|
||||||
|
val success = executeSusfsCommand(context, "enable_log $value")
|
||||||
|
if (success) {
|
||||||
|
saveEnableLogState(context, enabled)
|
||||||
|
|
||||||
|
// 如果开启了开机自启动,更新启动脚本
|
||||||
|
if (isAutoStartEnabled(context)) {
|
||||||
|
createStartupScript(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
if (enabled) context.getString(R.string.susfs_log_enabled) else context.getString(R.string.susfs_log_disabled),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取功能配置映射表
|
* 获取功能配置映射表
|
||||||
*/
|
*/
|
||||||
@@ -592,7 +642,9 @@ object SuSFSManager {
|
|||||||
val displayName = featureNameMap[mapping.id] ?: mapping.id
|
val displayName = featureNameMap[mapping.id] ?: mapping.id
|
||||||
val isEnabled = outputLines.contains(mapping.config)
|
val isEnabled = outputLines.contains(mapping.config)
|
||||||
val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled)
|
val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled)
|
||||||
features.add(EnabledFeature(displayName, isEnabled, statusText))
|
// 只有 enable_log 功能可以配置
|
||||||
|
val canConfigure = mapping.id == "status_enable_log"
|
||||||
|
features.add(EnabledFeature(displayName, isEnabled, statusText, canConfigure))
|
||||||
}
|
}
|
||||||
|
|
||||||
return features.sortedBy { it.name }
|
return features.sortedBy { it.name }
|
||||||
@@ -819,6 +871,27 @@ object SuSFSManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有任何配置可以启用开机自启动
|
||||||
|
*/
|
||||||
|
fun hasConfigurationForAutoStart(context: Context): Boolean {
|
||||||
|
val unameValue = getUnameValue(context)
|
||||||
|
val buildTimeValue = getBuildTimeValue(context)
|
||||||
|
val susPaths = getSusPaths(context)
|
||||||
|
val susMounts = getSusMounts(context)
|
||||||
|
val tryUmounts = getTryUmounts(context)
|
||||||
|
val enabledFeatures = runBlocking {
|
||||||
|
getEnabledFeatures(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (unameValue != DEFAULT_UNAME) ||
|
||||||
|
(buildTimeValue != DEFAULT_BUILD_TIME) ||
|
||||||
|
susPaths.isNotEmpty() ||
|
||||||
|
susMounts.isNotEmpty() ||
|
||||||
|
tryUmounts.isNotEmpty() ||
|
||||||
|
enabledFeatures.any { it.isEnabled }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置开机自启动
|
* 配置开机自启动
|
||||||
*/
|
*/
|
||||||
@@ -826,11 +899,7 @@ object SuSFSManager {
|
|||||||
try {
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// 启用开机自启动
|
// 启用开机自启动
|
||||||
val lastValue = getLastAppliedValue(context)
|
if (!hasConfigurationForAutoStart(context)) {
|
||||||
val lastBuildTime = getLastAppliedBuildTime(context)
|
|
||||||
if (lastValue == DEFAULT_UNAME && lastBuildTime == DEFAULT_BUILD_TIME &&
|
|
||||||
getSusPaths(context).isEmpty() && getSusMounts(context).isEmpty() &&
|
|
||||||
getTryUmounts(context).isEmpty()) {
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -416,7 +416,7 @@
|
|||||||
<string name="susfs_binary_not_found">无法找到 ksu_susfs 文件</string>
|
<string name="susfs_binary_not_found">无法找到 ksu_susfs 文件</string>
|
||||||
<string name="susfs_command_failed">SuSFS 命令执行失败</string>
|
<string name="susfs_command_failed">SuSFS 命令执行失败</string>
|
||||||
<string name="susfs_command_error">执行 SuSFS 命令时出错: %s</string>
|
<string name="susfs_command_error">执行 SuSFS 命令时出错: %s</string>
|
||||||
<string name="susfs_uname_set_success">SuSFS uname 和构建时间设置成功: %s, %s</string>
|
<string name="susfs_uname_set_success" formatted="false">SuSFS 内核名称和构建时间设置成功: %s, %s</string>
|
||||||
<!-- SuSFS Settings Item -->
|
<!-- SuSFS Settings Item -->
|
||||||
<string name="susfs_config_setting_title">SuSFS 配置</string>
|
<string name="susfs_config_setting_title">SuSFS 配置</string>
|
||||||
<string name="susfs_config_setting_summary">配置 SuSFS 的 uname 值和构建时间伪装 (当前: %s)</string>
|
<string name="susfs_config_setting_summary">配置 SuSFS 的 uname 值和构建时间伪装 (当前: %s)</string>
|
||||||
@@ -501,4 +501,12 @@
|
|||||||
<string name="magic_mount_feature_label">魔法坐骑支持</string>
|
<string name="magic_mount_feature_label">魔法坐骑支持</string>
|
||||||
<string name="overlayfs_auto_kstat_feature_label">OverlayFS 自动内核统计支持</string>
|
<string name="overlayfs_auto_kstat_feature_label">OverlayFS 自动内核统计支持</string>
|
||||||
<string name="sus_kstat_feature_label">SUS Kstat 支持</string>
|
<string name="sus_kstat_feature_label">SUS Kstat 支持</string>
|
||||||
|
<!-- 可切换状态 -->
|
||||||
|
<string name="susfs_configure">SuSFS 配置</string>
|
||||||
|
<string name="susfs_feature_configurable">可配置的 SuSFS 功能</string>
|
||||||
|
<string name="susfs_enable_log_label">SuSFS 启用日志</string>
|
||||||
|
<string name="susfs_log_config_description">启用或者关闭SuSFS的日志</string>
|
||||||
|
<string name="susfs_log_config_title">SuSFS 日志配置</string>
|
||||||
|
<string name="susfs_log_enabled">启用 SuSFS 日志</string>
|
||||||
|
<string name="susfs_log_disabled">关闭 SuSFS 日志</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
<string name="susfs_binary_not_found">Cannot find ksu_susfs file</string>
|
<string name="susfs_binary_not_found">Cannot find ksu_susfs file</string>
|
||||||
<string name="susfs_command_failed">SuSFS command execution failed</string>
|
<string name="susfs_command_failed">SuSFS command execution failed</string>
|
||||||
<string name="susfs_command_error">Error executing SuSFS command: %s</string>
|
<string name="susfs_command_error">Error executing SuSFS command: %s</string>
|
||||||
<string name="susfs_uname_set_success">SuSFS uname and build time set successfully: %s, %s</string>
|
<string name="susfs_uname_set_success" formatted="false">SuSFS uname and build time set successfully: %s, %s</string>
|
||||||
<!-- SuSFS Settings Item -->
|
<!-- SuSFS Settings Item -->
|
||||||
<string name="susfs_config_setting_title">SuSFS Configuration</string>
|
<string name="susfs_config_setting_title">SuSFS Configuration</string>
|
||||||
<string name="susfs_config_setting_summary">Configure SuSFS uname value and build time spoofing (Current: %s)</string>
|
<string name="susfs_config_setting_summary">Configure SuSFS uname value and build time spoofing (Current: %s)</string>
|
||||||
@@ -503,4 +503,12 @@
|
|||||||
<string name="magic_mount_feature_label">Magic Mount Support</string>
|
<string name="magic_mount_feature_label">Magic Mount Support</string>
|
||||||
<string name="overlayfs_auto_kstat_feature_label">OverlayFS Auto Kernel Stat Support</string>
|
<string name="overlayfs_auto_kstat_feature_label">OverlayFS Auto Kernel Stat Support</string>
|
||||||
<string name="sus_kstat_feature_label">SUS Kstat Support</string>
|
<string name="sus_kstat_feature_label">SUS Kstat Support</string>
|
||||||
|
<!-- 可切换状态 -->
|
||||||
|
<string name="susfs_configure">SuSFS Configuration</string>
|
||||||
|
<string name="susfs_feature_configurable">Configurable SuSFS Features</string>
|
||||||
|
<string name="susfs_enable_log_label">SuSFS Enable Log</string>
|
||||||
|
<string name="susfs_log_config_description">Enable or disable logging for SuSFS</string>
|
||||||
|
<string name="susfs_log_config_title">SuSFS Logging Configuration</string>
|
||||||
|
<string name="susfs_log_enabled">Enabling SuSFS Logging</string>
|
||||||
|
<string name="susfs_log_disabled">Turn off SuSFS logging</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user