From 9110d89d610dbfb476ab0c513a971f7804aa4672 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Sun, 15 Jun 2025 03:38:51 +0800 Subject: [PATCH] manager: Add configuration for SuSFS logging feature, support enable/disable logging --- .../com/sukisu/ultra/ui/screen/SuSFSConfig.kt | 420 +------------- .../extensions/SuSFSConfigExtensions.kt | 539 ++++++++++++++++++ .../com/sukisu/ultra/ui/util/SuSFSManager.kt | 87 ++- .../src/main/res/values-zh-rCN/strings.xml | 10 +- manager/app/src/main/res/values/strings.xml | 10 +- 5 files changed, 655 insertions(+), 411 deletions(-) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt index 858e895e..c03c4787 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt @@ -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, - 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, - 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, - 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 + enabledFeatures: List, + 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) - ) - } } } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt new file mode 100644 index 00000000..9fa260da --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/extensions/SuSFSConfigExtensions.kt @@ -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, + 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, + 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, + 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 + ) + } + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt index 2caf0e75..b649004e 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt @@ -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, diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index ecb6364a..7efc0fbf 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -416,7 +416,7 @@ 无法找到 ksu_susfs 文件 SuSFS 命令执行失败 执行 SuSFS 命令时出错: %s - SuSFS uname 和构建时间设置成功: %s, %s + SuSFS 内核名称和构建时间设置成功: %s, %s SuSFS 配置 配置 SuSFS 的 uname 值和构建时间伪装 (当前: %s) @@ -501,4 +501,12 @@ 魔法坐骑支持 OverlayFS 自动内核统计支持 SUS Kstat 支持 + + SuSFS 配置 + 可配置的 SuSFS 功能 + SuSFS 启用日志 + 启用或者关闭SuSFS的日志 + SuSFS 日志配置 + 启用 SuSFS 日志 + 关闭 SuSFS 日志 \ No newline at end of file diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 7fe54524..aa184aa4 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -418,7 +418,7 @@ Cannot find ksu_susfs file SuSFS command execution failed Error executing SuSFS command: %s - SuSFS uname and build time set successfully: %s, %s + SuSFS uname and build time set successfully: %s, %s SuSFS Configuration Configure SuSFS uname value and build time spoofing (Current: %s) @@ -503,4 +503,12 @@ Magic Mount Support OverlayFS Auto Kernel Stat Support SUS Kstat Support + + SuSFS Configuration + Configurable SuSFS Features + SuSFS Enable Log + Enable or disable logging for SuSFS + SuSFS Logging Configuration + Enabling SuSFS Logging + Turn off SuSFS logging \ No newline at end of file