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 30ca7187..3260ce5e 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 @@ -82,6 +82,7 @@ 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 com.sukisu.ultra.ui.screen.extensions.KstatConfigContent import kotlinx.coroutines.launch /** @@ -92,6 +93,7 @@ enum class SuSFSTab(val displayNameRes: Int) { SUS_PATHS(R.string.susfs_tab_sus_paths), SUS_MOUNTS(R.string.susfs_tab_sus_mounts), TRY_UMOUNT(R.string.susfs_tab_try_umount), + KSTAT_CONFIG(R.string.susfs_tab_kstat_config), PATH_SETTINGS(R.string.susfs_tab_path_settings), ENABLED_FEATURES(R.string.susfs_tab_enabled_features); @@ -136,6 +138,10 @@ fun SuSFSConfigScreen( var androidDataPath by remember { mutableStateOf("") } var sdcardPath by remember { mutableStateOf("") } + // Kstat配置相关状态 + var kstatConfigs by remember { mutableStateOf(emptySet()) } + var addKstatPaths by remember { mutableStateOf(emptySet()) } + // 启用功能状态相关 var enabledFeatures by remember { mutableStateOf(emptyList()) } var isLoadingFeatures by remember { mutableStateOf(false) } @@ -151,14 +157,32 @@ fun SuSFSConfigScreen( var newUmountMode by remember { mutableIntStateOf(0) } var umountModeExpanded by remember { mutableStateOf(false) } + // Kstat配置对话框状态 + var showAddKstatStaticallyDialog by remember { mutableStateOf(false) } + var showAddKstatDialog by remember { mutableStateOf(false) } + var newKstatPath by remember { mutableStateOf("") } + var newKstatIno by remember { mutableStateOf("") } + var newKstatDev by remember { mutableStateOf("") } + var newKstatNlink by remember { mutableStateOf("") } + var newKstatSize by remember { mutableStateOf("") } + var newKstatAtime by remember { mutableStateOf("") } + var newKstatAtimeNsec by remember { mutableStateOf("") } + var newKstatMtime by remember { mutableStateOf("") } + var newKstatMtimeNsec by remember { mutableStateOf("") } + var newKstatCtime by remember { mutableStateOf("") } + var newKstatCtimeNsec by remember { mutableStateOf("") } + var newKstatBlocks by remember { mutableStateOf("") } + var newKstatBlksize by remember { mutableStateOf("") } + // 重置确认对话框状态 var showResetPathsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) } + var showResetKstatDialog by remember { mutableStateOf(false) } val allTabs = SuSFSTab.getAllTabs() - // 实时判断是否可以启用开机自启动 - 修改逻辑 + // 实时判断是否可以启用开机自启动 val canEnableAutoStart by remember { derivedStateOf { SuSFSManager.hasConfigurationForAutoStart(context) @@ -197,6 +221,8 @@ fun SuSFSConfigScreen( tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) sdcardPath = SuSFSManager.getSdcardPath(context) + kstatConfigs = SuSFSManager.getKstatConfigs(context) + addKstatPaths = SuSFSManager.getAddKstatPaths(context) // 加载槽位信息 loadSlotInfo() @@ -581,6 +607,313 @@ fun SuSFSConfigScreen( ) } + // 添加Kstat静态配置对话框 + if (showAddKstatStaticallyDialog) { + AlertDialog( + onDismissRequest = { showAddKstatStaticallyDialog = false }, + title = { + Text( + stringResource(R.string.add_kstat_statically_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatPath, + onValueChange = { newKstatPath = it }, + label = { Text(stringResource(R.string.file_or_directory_path_label)) }, + placeholder = { Text("/path/to/file_or_directory") }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatIno, + onValueChange = { newKstatIno = it }, + label = { Text("ino") }, + placeholder = { Text("1234") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatDev, + onValueChange = { newKstatDev = it }, + label = { Text("dev") }, + placeholder = { Text("1234") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatNlink, + onValueChange = { newKstatNlink = it }, + label = { Text("nlink") }, + placeholder = { Text("2") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatSize, + onValueChange = { newKstatSize = it }, + label = { Text("size") }, + placeholder = { Text("223344") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatAtime, + onValueChange = { newKstatAtime = it }, + label = { Text("atime") }, + placeholder = { Text("1712592355") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatAtimeNsec, + onValueChange = { newKstatAtimeNsec = it }, + label = { Text("atime_nsec") }, + placeholder = { Text("0") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatMtime, + onValueChange = { newKstatMtime = it }, + label = { Text("mtime") }, + placeholder = { Text("1712592355") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatMtimeNsec, + onValueChange = { newKstatMtimeNsec = it }, + label = { Text("mtime_nsec") }, + placeholder = { Text("0") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatCtime, + onValueChange = { newKstatCtime = it }, + label = { Text("ctime") }, + placeholder = { Text("1712592355") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatCtimeNsec, + onValueChange = { newKstatCtimeNsec = it }, + label = { Text("ctime_nsec") }, + placeholder = { Text("0") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = newKstatBlocks, + onValueChange = { newKstatBlocks = it }, + label = { Text("blocks") }, + placeholder = { Text("16") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + OutlinedTextField( + value = newKstatBlksize, + onValueChange = { newKstatBlksize = it }, + label = { Text("blksize") }, + placeholder = { Text("512") }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) + } + + Text( + text = stringResource(R.string.hint_use_default_value), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + confirmButton = { + Button( + onClick = { + if (newKstatPath.isNotBlank()) { + coroutineScope.launch { + isLoading = true + if (SuSFSManager.addKstatStatically( + context, newKstatPath.trim(), + newKstatIno.trim().ifBlank { "default" }, + newKstatDev.trim().ifBlank { "default" }, + newKstatNlink.trim().ifBlank { "default" }, + newKstatSize.trim().ifBlank { "default" }, + newKstatAtime.trim().ifBlank { "default" }, + newKstatAtimeNsec.trim().ifBlank { "default" }, + newKstatMtime.trim().ifBlank { "default" }, + newKstatMtimeNsec.trim().ifBlank { "default" }, + newKstatCtime.trim().ifBlank { "default" }, + newKstatCtimeNsec.trim().ifBlank { "default" }, + newKstatBlocks.trim().ifBlank { "default" }, + newKstatBlksize.trim().ifBlank { "default" } + )) { + kstatConfigs = SuSFSManager.getKstatConfigs(context) + } + isLoading = false + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + showAddKstatStaticallyDialog = false + } + } + }, + enabled = newKstatPath.isNotBlank() && !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text("添加") + } + }, + dismissButton = { + TextButton( + onClick = { + showAddKstatStaticallyDialog = false + // 清空所有字段 + newKstatPath = "" + newKstatIno = "" + newKstatDev = "" + newKstatNlink = "" + newKstatSize = "" + newKstatAtime = "" + newKstatAtimeNsec = "" + newKstatMtime = "" + newKstatMtimeNsec = "" + newKstatCtime = "" + newKstatCtimeNsec = "" + newKstatBlocks = "" + newKstatBlksize = "" + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + + // 添加Kstat路径对话框 + if (showAddKstatDialog) { + AlertDialog( + onDismissRequest = { showAddKstatDialog = false }, + title = { + Text( + stringResource(R.string.add_kstat_path_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = newKstatPath, + onValueChange = { newKstatPath = it }, + label = { Text(stringResource(R.string.file_or_directory_path_label)) }, + placeholder = { Text("/path/to/file_or_directory") }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + + Text( + text = stringResource(R.string.kstat_command_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + confirmButton = { + Button( + onClick = { + if (newKstatPath.isNotBlank()) { + coroutineScope.launch { + isLoading = true + if (SuSFSManager.addKstat(context, newKstatPath.trim())) { + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + } + isLoading = false + newKstatPath = "" + showAddKstatDialog = false + } + } + }, + enabled = newKstatPath.isNotBlank() && !isLoading, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.add)) + } + }, + dismissButton = { + TextButton( + onClick = { + showAddKstatDialog = false + newKstatPath = "" + }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + // 运行尝试卸载确认对话框 if (showRunUmountDialog) { AlertDialog( @@ -762,6 +1095,55 @@ fun SuSFSConfigScreen( ) } + // 重置Kstat配置确认对话框 + if (showResetKstatDialog) { + AlertDialog( + onDismissRequest = { showResetKstatDialog = false }, + title = { + Text( + stringResource(R.string.reset_kstat_config_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + }, + text = { Text(stringResource(R.string.reset_kstat_config_message)) }, + confirmButton = { + Button( + onClick = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveKstatConfigs(context, emptySet()) + SuSFSManager.saveAddKstatPaths(context, emptySet()) + kstatConfigs = emptySet() + addKstatPaths = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetKstatDialog = false + } + }, + enabled = !isLoading, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ), + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.confirm_reset)) + } + }, + dismissButton = { + TextButton( + onClick = { showResetKstatDialog = false }, + shape = RoundedCornerShape(8.dp) + ) { + Text(stringResource(R.string.cancel)) + } + }, + shape = RoundedCornerShape(12.dp) + ) + } + // 重置确认对话框 if (showConfirmReset) { AlertDialog( @@ -985,6 +1367,29 @@ fun SuSFSConfigScreen( } } + SuSFSTab.KSTAT_CONFIG -> { + // 重置按钮 + OutlinedButton( + onClick = { showResetKstatDialog = true }, + enabled = !isLoading && (kstatConfigs.isNotEmpty() || addKstatPaths.isNotEmpty()), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.reset_kstat_config_title), + fontWeight = FontWeight.Medium + ) + } + } + SuSFSTab.PATH_SETTINGS -> { // 重置按钮 OutlinedButton( @@ -1160,6 +1565,47 @@ fun SuSFSConfigScreen( } ) } + SuSFSTab.KSTAT_CONFIG -> { + KstatConfigContent( + kstatConfigs = kstatConfigs, + addKstatPaths = addKstatPaths, + isLoading = isLoading, + onAddKstatStatically = { showAddKstatStaticallyDialog = true }, + onAddKstat = { showAddKstatDialog = true }, + onRemoveKstatConfig = { config -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeKstatConfig(context, config)) { + kstatConfigs = SuSFSManager.getKstatConfigs(context) + } + isLoading = false + } + }, + onRemoveAddKstat = { path -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeAddKstat(context, path)) { + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + } + isLoading = false + } + }, + onUpdateKstat = { path -> + coroutineScope.launch { + isLoading = true + SuSFSManager.updateKstat(context, path) + isLoading = false + } + }, + onUpdateKstatFullClone = { path -> + coroutineScope.launch { + isLoading = true + SuSFSManager.updateKstatFullClone(context, path) + isLoading = false + } + } + ) + } SuSFSTab.PATH_SETTINGS -> { PathSettingsContent( androidDataPath = androidDataPath, 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 index 9fa260da..a68d8c31 100644 --- 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 @@ -20,6 +20,8 @@ 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.material.icons.filled.Update +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -175,6 +177,166 @@ fun PathItemCard( } } +/** + * Kstat配置项目卡片组件 + */ +@Composable +fun KstatConfigItemCard( + config: String, + onDelete: () -> Unit, + isLoading: Boolean = false +) { + 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 = Icons.Default.Settings, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column { + val parts = config.split("|") + if (parts.isNotEmpty()) { + Text( + text = parts[0], // 路径 + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + if (parts.size > 1) { + Text( + text = "参数: ${parts.drop(1).joinToString(" ")}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + Text( + text = config, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + } + } + } + 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) + ) + } + } + } +} + +/** + * Add Kstat路径项目卡片组件 + */ +@Composable +fun AddKstatPathItemCard( + path: String, + onDelete: () -> Unit, + onUpdate: () -> Unit, + onUpdateFullClone: () -> Unit, + isLoading: Boolean = false +) { + 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 = Icons.Default.Folder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = path, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + IconButton( + onClick = onUpdate, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.Update, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(16.dp) + ) + } + IconButton( + onClick = onUpdateFullClone, + enabled = !isLoading, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(16.dp) + ) + } + 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) + ) + } + } + } + } +} + /** * 启用功能状态卡片组件 */ @@ -536,4 +698,158 @@ fun TryUmountContent( } } } +} + +/** + * Kstat配置内容组件 + */ +@Composable +fun KstatConfigContent( + kstatConfigs: Set, + addKstatPaths: Set, + isLoading: Boolean, + onAddKstatStatically: () -> Unit, + onAddKstat: () -> Unit, + onRemoveKstatConfig: (String) -> Unit, + onRemoveAddKstat: (String) -> Unit, + onUpdateKstat: (String) -> Unit, + onUpdateKstatFullClone: (String) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 标题和添加按钮 + UnifiedButtonRow( + primaryButton = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FloatingActionButton( + onClick = onAddKstatStatically, + modifier = Modifier.size(48.dp), + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + FloatingActionButton( + onClick = onAddKstat, + modifier = Modifier.size(48.dp), + containerColor = MaterialTheme.colorScheme.secondary, + contentColor = MaterialTheme.colorScheme.onSecondary + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + } + }, + secondaryButtons = { + Text( + text = stringResource(R.string.kstat_config_management), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + ) + + // 说明卡片 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.kstat_config_description_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.kstat_config_description_add_statically), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.kstat_config_description_add), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.kstat_config_description_update), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.kstat_config_description_update_full_clone), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + +// 静态Kstat配置列表 + if (kstatConfigs.isNotEmpty()) { + Text( + text = stringResource(R.string.static_kstat_config), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + LazyColumn( + modifier = Modifier.weight(0.5f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(kstatConfigs.toList()) { config -> + KstatConfigItemCard( + config = config, + onDelete = { onRemoveKstatConfig(config) }, + isLoading = isLoading + ) + } + } + } + +// Add Kstat路径列表 + if (addKstatPaths.isNotEmpty()) { + Text( + text = stringResource(R.string.kstat_path_management), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + LazyColumn( + modifier = Modifier.weight(0.5f), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(addKstatPaths.toList()) { path -> + AddKstatPathItemCard( + path = path, + onDelete = { onRemoveAddKstat(path) }, + onUpdate = { onUpdateKstat(path) }, + onUpdateFullClone = { onUpdateKstatFullClone(path) }, + isLoading = isLoading + ) + } + } + } + +// 空状态显示 + if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) { + EmptyStateCard( + message = stringResource(R.string.no_kstat_config_message) + ) + } + } } \ 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 6e1657ce..6fe28a43 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 @@ -8,7 +8,9 @@ import com.dergoogler.mmrl.platform.Platform.Companion.context import com.sukisu.ultra.Natives import com.sukisu.ultra.R import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.FileOutputStream @@ -34,6 +36,8 @@ object SuSFSManager { private const val KEY_SDCARD_PATH = "sdcard_path" private const val KEY_ENABLE_LOG = "enable_log" private const val KEY_EXECUTE_IN_POST_FS_DATA = "execute_in_post_fs_data" + private const val KEY_KSTAT_CONFIGS = "kstat_configs" + private const val KEY_ADD_KSTAT_PATHS = "add_kstat_paths" private const val SUSFS_BINARY_BASE_NAME = "ksu_susfs" private const val DEFAULT_UNAME = "default" private const val DEFAULT_BUILD_TIME = "default" @@ -198,13 +202,56 @@ object SuSFSManager { } /** - * 保存执行位置设置 + * 保存执行位置设置并实时更新状态 */ fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) { + val oldExecuteInPostFsData = getExecuteInPostFsData(context) + getPrefs(context).edit().apply { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) apply() } + + if (oldExecuteInPostFsData != executeInPostFsData && isAutoStartEnabled(context)) { + CoroutineScope(Dispatchers.Default).launch { + try { + // 删除旧的模块状态 + removeMagiskModule() + + // 重新创建模块以应用新的执行位置设置 + val success = createMagiskModule(context) + + withContext(Dispatchers.Main) { + if (success) { + Toast.makeText( + context, + if (executeInPostFsData) { + context.getString(R.string.susfs_execute_location_updated_post_fs_data) + } else { + context.getString(R.string.susfs_execute_location_updated_service) + }, + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.susfs_execute_location_update_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { + Toast.makeText( + context, + context.getString(R.string.susfs_execute_location_update_error, e.message ?: "Unknown error"), + Toast.LENGTH_SHORT + ).show() + } + } + } + } } /** @@ -343,6 +390,40 @@ object SuSFSManager { return getPrefs(context).getStringSet(KEY_TRY_UMOUNTS, emptySet()) ?: emptySet() } + /** + * 保存Kstat配置列表 + */ + fun saveKstatConfigs(context: Context, configs: Set) { + getPrefs(context).edit().apply { + putStringSet(KEY_KSTAT_CONFIGS, configs) + apply() + } + } + + /** + * 获取Kstat配置列表 + */ + fun getKstatConfigs(context: Context): Set { + return getPrefs(context).getStringSet(KEY_KSTAT_CONFIGS, emptySet()) ?: emptySet() + } + + /** + * 保存Add Kstat路径列表 + */ + fun saveAddKstatPaths(context: Context, paths: Set) { + getPrefs(context).edit().apply { + putStringSet(KEY_ADD_KSTAT_PATHS, paths) + apply() + } + } + + /** + * 获取Add Kstat路径列表 + */ + fun getAddKstatPaths(context: Context): Set { + return getPrefs(context).getStringSet(KEY_ADD_KSTAT_PATHS, emptySet()) ?: emptySet() + } + /** * 保存Android Data路径 */ @@ -462,11 +543,14 @@ object SuSFSManager { val androidDataPath = getAndroidDataPath(context) val sdcardPath = getSdcardPath(context) val enableLog = getEnableLogState(context) + val kstatConfigs = getKstatConfigs(context) + val addKstatPaths = getAddKstatPaths(context) // 生成并创建service.sh val serviceScript = ScriptGenerator.generateServiceScript( targetPath, unameValue, buildTimeValue, susPaths, - androidDataPath, sdcardPath, enableLog, executeInPostFsData + androidDataPath, sdcardPath, enableLog, executeInPostFsData, + kstatConfigs, addKstatPaths ) val createServiceResult = shell.newJob() .add("cat > $MODULE_PATH/service.sh << 'EOF'\n$serviceScript\nEOF") @@ -794,6 +878,133 @@ object SuSFSManager { return executeSusfsCommand(context, "run_try_umount") } + /** + * 添加Kstat静态配置 + */ + suspend fun addKstatStatically( + context: Context, + path: String, + ino: String, + dev: String, + nlink: String, + size: String, + atime: String, + atimeNsec: String, + mtime: String, + mtimeNsec: String, + ctime: String, + ctimeNsec: String, + blocks: String, + blksize: String + ): Boolean { + val command = "add_sus_kstat_statically '$path' '$ino' '$dev' '$nlink' '$size' '$atime' '$atimeNsec' '$mtime' '$mtimeNsec' '$ctime' '$ctimeNsec' '$blocks' '$blksize'" + val success = executeSusfsCommand(context, command) + if (success) { + val currentConfigs = getKstatConfigs(context).toMutableSet() + val configEntry = "$path|$ino|$dev|$nlink|$size|$atime|$atimeNsec|$mtime|$mtimeNsec|$ctime|$ctimeNsec|$blocks|$blksize" + currentConfigs.add(configEntry) + saveKstatConfigs(context, currentConfigs) + + // 如果开启了开机自启动,更新模块 + if (isAutoStartEnabled(context)) { + createMagiskModule(context) + } + + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_static_config_added, path), Toast.LENGTH_SHORT).show() + } + } + return success + } + + /** + * 移除Kstat配置 + */ + suspend fun removeKstatConfig(context: Context, config: String): Boolean { + val currentConfigs = getKstatConfigs(context).toMutableSet() + currentConfigs.remove(config) + saveKstatConfigs(context, currentConfigs) + + // 如果开启了开机自启动,更新模块 + if (isAutoStartEnabled(context)) { + createMagiskModule(context) + } + + val parts = config.split("|") + val path = if (parts.isNotEmpty()) parts[0] else config + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_config_removed, path), Toast.LENGTH_SHORT).show() + } + return true + } + + /** + * 添加Kstat路径 + */ + suspend fun addKstat(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_kstat '$path'") + if (success) { + val currentPaths = getAddKstatPaths(context).toMutableSet() + currentPaths.add(path) + saveAddKstatPaths(context, currentPaths) + + // 如果开启了开机自启动,更新模块 + if (isAutoStartEnabled(context)) { + createMagiskModule(context) + } + + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_path_added, path), Toast.LENGTH_SHORT).show() + } + } + return success + } + + /** + * 移除Add Kstat路径 + */ + suspend fun removeAddKstat(context: Context, path: String): Boolean { + val currentPaths = getAddKstatPaths(context).toMutableSet() + currentPaths.remove(path) + saveAddKstatPaths(context, currentPaths) + + // 如果开启了开机自启动,更新模块 + if (isAutoStartEnabled(context)) { + createMagiskModule(context) + } + + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_path_removed, path), Toast.LENGTH_SHORT).show() + } + return true + } + + /** + * 更新Kstat + */ + suspend fun updateKstat(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "update_sus_kstat '$path'") + if (success) { + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_updated, path), Toast.LENGTH_SHORT).show() + } + } + return success + } + + /** + * 完整克隆更新Kstat + */ + suspend fun updateKstatFullClone(context: Context, path: String): Boolean { + val success = executeSusfsCommand(context, "update_sus_kstat_full_clone '$path'") + if (success) { + withContext(Dispatchers.Main) { + Toast.makeText(context, context.getString(R.string.kstat_full_clone_updated, path), Toast.LENGTH_SHORT).show() + } + } + return success + } + /** * 设置Android Data路径 */ @@ -905,6 +1116,8 @@ object SuSFSManager { val susPaths = getSusPaths(context) val susMounts = getSusMounts(context) val tryUmounts = getTryUmounts(context) + val kstatConfigs = getKstatConfigs(context) + val addKstatPaths = getAddKstatPaths(context) val enabledFeatures = runBlocking { getEnabledFeatures(context) } @@ -914,6 +1127,8 @@ object SuSFSManager { susPaths.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty() || + kstatConfigs.isNotEmpty() || + addKstatPaths.isNotEmpty() || enabledFeatures.any { it.isEnabled } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt index 45f7a81e..711a8b4e 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt @@ -20,7 +20,9 @@ object ScriptGenerator { androidDataPath: String, sdcardPath: String, enableLog: Boolean, - executeInPostFsData: Boolean = false + executeInPostFsData: Boolean = false, + kstatConfigs: Set = emptySet(), + addKstatPaths: Set = emptySet() ): String { return buildString { appendLine("#!/system/bin/sh") @@ -42,7 +44,7 @@ object ScriptGenerator { appendLine("# 检查SuSFS二进制文件") appendLine("SUSFS_BIN=\"$targetPath\"") appendLine("if [ ! -f \"\$SUSFS_BIN\" ]; then") - appendLine(" echo \"\$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") + appendLine(" echo \"$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") appendLine(" exit 1") appendLine("fi") appendLine() @@ -51,14 +53,14 @@ object ScriptGenerator { appendLine("# 设置日志启用状态") val logValue = if (enableLog) 1 else 0 appendLine("\"\$SUSFS_BIN\" enable_log $logValue") - appendLine("echo \"\$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"\$LOG_FILE\"") appendLine() // 设置Android Data路径 if (androidDataPath != "/sdcard/Android/data") { appendLine("# 设置Android Data路径") appendLine("\"\$SUSFS_BIN\" set_android_data_root_path '$androidDataPath'") - appendLine("echo \"\$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"\$LOG_FILE\"") appendLine() } @@ -66,7 +68,7 @@ object ScriptGenerator { if (sdcardPath != "/sdcard") { appendLine("# 设置SD卡路径") appendLine("\"\$SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'") - appendLine("echo \"\$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"\$LOG_FILE\"") appendLine() } @@ -75,7 +77,32 @@ object ScriptGenerator { appendLine("# 添加SUS路径") susPaths.forEach { path -> appendLine("\"\$SUSFS_BIN\" add_sus_path '$path'") - appendLine("echo \"\$(get_current_time): 添加SUS路径: $path\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"\$LOG_FILE\"") + } + appendLine() + } + + // 添加Kstat静态配置 + if (kstatConfigs.isNotEmpty()) { + appendLine("# 添加Kstat静态配置") + kstatConfigs.forEach { config -> + val parts = config.split("|") + if (parts.size >= 13) { + val path = parts[0] + val params = parts.drop(1).joinToString("' '", "'", "'") + appendLine("\"\$SUSFS_BIN\" add_sus_kstat_statically $params") + appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"\$LOG_FILE\"") + } + } + appendLine() + } + + // 添加Kstat路径 + if (addKstatPaths.isNotEmpty()) { + appendLine("# 添加Kstat路径") + addKstatPaths.forEach { path -> + appendLine("\"\$SUSFS_BIN\" add_sus_kstat '$path'") + appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"\$LOG_FILE\"") } appendLine() } @@ -84,38 +111,38 @@ object ScriptGenerator { if (!executeInPostFsData && (unameValue != "default" || buildTimeValue != "default")) { appendLine("# 设置uname和构建时间") appendLine("\"\$SUSFS_BIN\" set_uname '$unameValue' '$buildTimeValue'") - appendLine("echo \"\$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"\$LOG_FILE\"") appendLine() } appendLine("# 隐弱BL 来自 Shamiko 脚本") appendLine("check_reset_prop() {") - appendLine("local NAME=\$1") - appendLine("local EXPECTED=\$2") - appendLine("local VALUE=\$(resetprop \$NAME)") + appendLine("local NAME=$1") + appendLine("local EXPECTED=$2") + appendLine("local VALUE=$(resetprop \$NAME)") appendLine("[ -z \$VALUE ] || [ \$VALUE = \$EXPECTED ] || resetprop \$NAME \$EXPECTED") appendLine("}") appendLine() appendLine("check_missing_prop() {") - appendLine(" local NAME=\$1") - appendLine(" local EXPECTED=\$2") - appendLine(" local VALUE=\$(resetprop \$NAME)") + appendLine(" local NAME=$1") + appendLine(" local EXPECTED=$2") + appendLine(" local VALUE=$(resetprop \$NAME)") appendLine(" [ -z \$VALUE ] && resetprop \$NAME \$EXPECTED") appendLine("}") appendLine() appendLine("check_missing_match_prop() {") - appendLine(" local NAME=\$1") - appendLine(" local EXPECTED=\$2") - appendLine(" local VALUE=\$(resetprop \$NAME)") + appendLine(" local NAME=$1") + appendLine(" local EXPECTED=$2") + appendLine(" local VALUE=$(resetprop \$NAME)") appendLine(" [ -z \$VALUE ] || [ \$VALUE = \$EXPECTED ] || resetprop \$NAME \$EXPECTED") appendLine(" [ -z \$VALUE ] && resetprop \$NAME \$EXPECTED") appendLine("}") appendLine() appendLine("contains_reset_prop() {") - appendLine("local NAME=\$1") - appendLine("local CONTAINS=\$2") - appendLine("local NEWVAL=\$3") - appendLine("[[ \"\$(resetprop \$NAME)\" = *\"\$CONTAINS\"* ]] && resetprop \$NAME \$NEWVAL") + appendLine("local NAME=$1") + appendLine("local CONTAINS=$2") + appendLine("local NEWVAL=$3") + appendLine("[[ \"$(resetprop \$NAME)\" = *\"\$CONTAINS\"* ]] && resetprop \$NAME \$NEWVAL") appendLine("}") appendLine() appendLine("resetprop -w sys.boot_completed 0") @@ -163,13 +190,13 @@ object ScriptGenerator { appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"") appendLine() appendLine("# Hide cloudphone detection") - appendLine("[ -n \"\$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"") + appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"") appendLine() appendLine("# fake encryption status") appendLine("check_reset_prop \"ro.crypto.state\" \"encrypted\"") appendLine() - appendLine("echo \"\$(get_current_time): Service脚本执行完成\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"\$LOG_FILE\"") } } @@ -202,22 +229,22 @@ object ScriptGenerator { appendLine("# 检查SuSFS二进制文件") appendLine("SUSFS_BIN=\"$targetPath\"") appendLine("if [ ! -f \"\$SUSFS_BIN\" ]; then") - appendLine(" echo \"\$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") + appendLine(" echo \"$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") appendLine(" exit 1") appendLine("fi") appendLine() - appendLine("echo \"\$(get_current_time): Post-FS-Data脚本开始执行\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"\$LOG_FILE\"") appendLine() // 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行 if (executeInPostFsData && (unameValue != "default" || buildTimeValue != "default")) { appendLine("# 设置uname和构建时间") appendLine("\"\$SUSFS_BIN\" set_uname '$unameValue' '$buildTimeValue'") - appendLine("echo \"\$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"\$LOG_FILE\"") appendLine() } - appendLine("echo \"\$(get_current_time): Post-FS-Data脚本执行完成\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"\$LOG_FILE\"") } } @@ -246,12 +273,12 @@ object ScriptGenerator { appendLine(" date '+%Y-%m-%d %H:%M:%S'") appendLine("}") appendLine() - appendLine("echo \"\$(get_current_time): Post-Mount脚本开始执行\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"\$LOG_FILE\"") appendLine() appendLine("# 检查SuSFS二进制文件") appendLine("SUSFS_BIN=\"$targetPath\"") appendLine("if [ ! -f \"\$SUSFS_BIN\" ]; then") - appendLine(" echo \"\$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") + appendLine(" echo \"$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") appendLine(" exit 1") appendLine("fi") appendLine() @@ -261,7 +288,7 @@ object ScriptGenerator { appendLine("# 添加SUS挂载") susMounts.forEach { mount -> appendLine("\"\$SUSFS_BIN\" add_sus_mount '$mount'") - appendLine("echo \"\$(get_current_time): 添加SUS挂载: $mount\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"\$LOG_FILE\"") } appendLine() } @@ -275,13 +302,13 @@ object ScriptGenerator { val path = parts[0] val mode = parts[1] appendLine("\"\$SUSFS_BIN\" add_try_umount '$path' $mode") - appendLine("echo \"\$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"\$LOG_FILE\"") } } appendLine() } - appendLine("echo \"\$(get_current_time): Post-Mount脚本执行完成\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"\$LOG_FILE\"") } } @@ -306,16 +333,16 @@ object ScriptGenerator { appendLine(" date '+%Y-%m-%d %H:%M:%S'") appendLine("}") appendLine() - appendLine("echo \"\$(get_current_time): Boot-Completed脚本开始执行\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"\$LOG_FILE\"") appendLine() appendLine("# 检查SuSFS二进制文件") appendLine("SUSFS_BIN=\"$targetPath\"") appendLine("if [ ! -f \"\$SUSFS_BIN\" ]; then") - appendLine(" echo \"\$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") + appendLine(" echo \"$(get_current_time): SuSFS二进制文件未找到: \$SUSFS_BIN\" >> \"\$LOG_FILE\"") appendLine(" exit 1") appendLine("fi") appendLine() - appendLine("echo \"\$(get_current_time): Boot-Completed脚本执行完成\" >> \"\$LOG_FILE\"") + appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"\$LOG_FILE\"") } } 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 8cfcc524..a3f1789e 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -543,4 +543,34 @@ SuSFS自启动模块已启用,模块路径:%s SuSFS自启动模块已禁用 + 执行位置已更新到 post-fs-data + 执行位置已更新到 service + 执行位置更新失败 + 更新执行位置时出错:%s + + Kstat 配置 + Kstat静态配置已添加: %1$s + 已移除Kstat配置: %1$s + Kstat路径已添加: %1$s + 已移除Kstat路径: %1$s + Kstat已更新: %1$s + Kstat完整克隆已更新: %1$s + 添加Kstat静态配置 + 文件/目录路径 + 提示:可以使用 ”default“ 来使用原始值 + 添加Kstat路径 + 此命令用于在路径被绑定挂载或覆盖之前添加,用于在内核内存中存储原始stat信息。 + 添加 + 重置Kstat配置 + 确定要清除所有Kstat配置吗?此操作不可撤销。 + 确认重置 + Kstat配置管理 + Kstat配置说明 + • add_sus_kstat_statically: 静态配置文件/目录的stat信息 + • add_sus_kstat: 在绑定挂载前添加路径,存储原始stat信息 + • update_sus_kstat: 更新目标ino,保持size和blocks不变 + • update_sus_kstat_full_clone: 仅更新ino,其他保持原始值 + 静态Kstat配置 + Kstat路径管理 + 暂无Kstat配置,点击上方按钮添加配置 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index cfb5173c..ddb70e92 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -545,4 +545,34 @@ SuSFS auto-start module enabled, module path: %s SuSFS auto-start module disabled + Execution location updated to post-fs-data + Execution location updated to service + Failed to update execution location + Error updating execution location: %s + + Kstat Configuration + Kstat static configuration added: %1$s + Kstat configuration removed: %1$s + Kstat path added: %1$s + Kstat path removed: %1$s + Kstat updated: %1$s + Kstat full clone updated: %1$s + Add Kstat Static Configuration + File/Directory Path + Hint: You can use ”default“ to use the original value + Add Kstat Path + This command is used to add before the path is bind-mounted or overlaid, storing the original stat information in kernel memory. + Add + Reset Kstat Configuration + Are you sure you want to clear all Kstat configurations? This action cannot be undone. + Confirm Reset + Kstat Configuration Management + Kstat Configuration Description + • add_sus_kstat_statically: Static stat info of files/directories + • add_sus_kstat: Add path before bind mount, storing original stat info + • update_sus_kstat: Update target ino, keep size and blocks unchanged + • update_sus_kstat_full_clone: Update ino only, keep other original values + Static Kstat Configuration + Kstat Path Management + No Kstat configuration yet, click the button above to add \ No newline at end of file