manager: Add configuration for SuSFS logging feature, support enable/disable logging

This commit is contained in:
ShirkNeko
2025-06-15 03:38:51 +08:00
parent 7b314116e9
commit 9110d89d61
5 changed files with 655 additions and 411 deletions

View File

@@ -23,16 +23,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
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.Delete
import androidx.compose.material.icons.filled.Folder
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.RestoreFromTrash
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -43,7 +38,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -82,6 +76,11 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
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
/**
@@ -151,12 +150,10 @@ fun SuSFSConfigScreen(
val allTabs = SuSFSTab.getAllTabs()
// 实时判断是否可以启用开机自启动
// 实时判断是否可以启用开机自启动 - 修改逻辑
val canEnableAutoStart by remember {
derivedStateOf {
(unameValue.trim().isNotBlank() && unameValue.trim() != "default") ||
(buildTimeValue.trim().isNotBlank() && buildTimeValue.trim() != "default") ||
susPaths.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty()
SuSFSManager.hasConfigurationForAutoStart(context)
}
}
@@ -190,7 +187,7 @@ fun SuSFSConfigScreen(
}
}
// 当输入值变化时,自动调整开机自启动状态
// 当配置变化时,自动调整开机自启动状态
LaunchedEffect(canEnableAutoStart) {
if (!canEnableAutoStart && autoStartEnabled) {
autoStartEnabled = false
@@ -742,7 +739,7 @@ fun SuSFSConfigScreen(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
stringResource(R.string.susfs_reset_to_default),
fontWeight = FontWeight.Medium
)
}
@@ -765,7 +762,7 @@ fun SuSFSConfigScreen(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
stringResource(R.string.susfs_reset_paths_title),
fontWeight = FontWeight.Medium
)
}
@@ -788,7 +785,7 @@ fun SuSFSConfigScreen(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
stringResource(R.string.susfs_reset_mounts_title),
fontWeight = FontWeight.Medium
)
}
@@ -811,7 +808,7 @@ fun SuSFSConfigScreen(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
stringResource(R.string.susfs_reset_umounts_title),
fontWeight = FontWeight.Medium
)
}
@@ -843,7 +840,7 @@ fun SuSFSConfigScreen(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset, stringResource(selectedTab.displayNameRes)),
stringResource(R.string.susfs_path_settings),
fontWeight = FontWeight.Medium
)
}
@@ -1014,7 +1011,8 @@ fun SuSFSConfigScreen(
}
SuSFSTab.ENABLED_FEATURES -> {
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
private fun EnabledFeaturesContent(
enabledFeatures: List<SuSFSManager.EnabledFeature>
enabledFeatures: List<SuSFSManager.EnabledFeature>,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
@@ -1543,182 +1333,12 @@ private fun EnabledFeaturesContent(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
items(enabledFeatures) { feature ->
FeatureStatusCard(feature = feature)
}
}
}
}
}
/**
* 统一的按钮布局组件
*/
@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
FeatureStatusCard(
feature = feature,
onRefresh = onRefresh
)
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)
)
}
}
}
}

View File

@@ -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
)
}
}
}
}
}

View File

@@ -4,12 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
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.sukisu.ultra.R
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.FileOutputStream
import java.io.IOException
@@ -32,6 +31,7 @@ object SuSFSManager {
private const val KEY_TRY_UMOUNTS = "try_umounts"
private const val KEY_ANDROID_DATA_PATH = "android_data_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 DEFAULT_UNAME = "default"
private const val DEFAULT_BUILD_TIME = "default"
@@ -63,7 +63,8 @@ object SuSFSManager {
data class EnabledFeature(
val name: String,
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)
}
/**
* 保存日志启用状态
*/
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路径列表
*/
@@ -343,6 +361,7 @@ object SuSFSManager {
val tryUmounts = getTryUmounts(context)
val androidDataPath = getAndroidDataPath(context)
val sdcardPath = getSdcardPath(context)
val enableLog = getEnableLogState(context)
val targetPath = getSuSFSTargetPath()
val scriptContent = buildString {
@@ -359,6 +378,12 @@ object SuSFSManager {
appendLine(" mkdir -p /data/adb/ksu/log")
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路径
if (androidDataPath != "/sdcard/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 isEnabled = outputLines.contains(mapping.config)
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 }
@@ -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 {
if (enabled) {
// 启用开机自启动
val lastValue = getLastAppliedValue(context)
val lastBuildTime = getLastAppliedBuildTime(context)
if (lastValue == DEFAULT_UNAME && lastBuildTime == DEFAULT_BUILD_TIME &&
getSusPaths(context).isEmpty() && getSusMounts(context).isEmpty() &&
getTryUmounts(context).isEmpty()) {
if (!hasConfigurationForAutoStart(context)) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,

View File

@@ -416,7 +416,7 @@
<string name="susfs_binary_not_found">无法找到 ksu_susfs 文件</string>
<string name="susfs_command_failed">SuSFS 命令执行失败</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 -->
<string name="susfs_config_setting_title">SuSFS 配置</string>
<string name="susfs_config_setting_summary">配置 SuSFS 的 uname 值和构建时间伪装 (当前: %s)</string>
@@ -501,4 +501,12 @@
<string name="magic_mount_feature_label">魔法坐骑支持</string>
<string name="overlayfs_auto_kstat_feature_label">OverlayFS 自动内核统计支持</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>

View File

@@ -418,7 +418,7 @@
<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_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 -->
<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>
@@ -503,4 +503,12 @@
<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="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>