manager: Implement editable and removable mount points for LKM
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.rounded.EnhancedEncryption
|
||||
@@ -43,6 +44,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
@@ -410,6 +412,19 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
)
|
||||
}
|
||||
val lkmMode = Natives.isLkmMode
|
||||
KsuIsValid {
|
||||
if (lkmMode) {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.FolderOff,
|
||||
title = stringResource(R.string.umount_path_manager),
|
||||
summary = stringResource(R.string.umount_path_manager_summary),
|
||||
onClick = {
|
||||
navigator.navigate(UmountManagerScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showBottomsheet) {
|
||||
LogBottomSheet(
|
||||
@@ -452,8 +467,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lkmMode = Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
@@ -1118,7 +1131,7 @@ fun SettingDropdown(
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowForward,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val SPACING_SMALL = 3.dp
|
||||
private val SPACING_MEDIUM = 8.dp
|
||||
private val SPACING_LARGE = 16.dp
|
||||
|
||||
data class UmountPathEntry(
|
||||
val path: String,
|
||||
val checkMnt: Boolean,
|
||||
val flags: Int,
|
||||
val isDefault: Boolean
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun UmountManagerScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
var pathList by remember { mutableStateOf<List<UmountPathEntry>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
|
||||
fun loadPaths() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
isLoading = true
|
||||
val result = listUmountPaths()
|
||||
val entries = parseUmountPaths(result)
|
||||
withContext(Dispatchers.Main) {
|
||||
pathList = entries
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
loadPaths()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.umount_path_manager)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.navigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { loadPaths() }) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = null)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(
|
||||
alpha = CardConfig.cardAlpha
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { showAddDialog = true }
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(SPACING_LARGE),
|
||||
colors = getCardColors(MaterialTheme.colorScheme.primaryContainer),
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(SPACING_LARGE)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||
Text(
|
||||
text = stringResource(R.string.umount_path_restart_notice),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||
verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
|
||||
) {
|
||||
items(pathList, key = { it.path }) { entry ->
|
||||
UmountPathCard(
|
||||
entry = entry,
|
||||
onDelete = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val success = removeUmountPath(entry.path)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.umount_path_removed)
|
||||
)
|
||||
loadPaths()
|
||||
} else {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.operation_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SPACING_LARGE),
|
||||
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (confirmDialog.awaitConfirm(
|
||||
title = context.getString(R.string.confirm_action),
|
||||
content = context.getString(R.string.confirm_clear_custom_paths)
|
||||
) == ConfirmResult.Confirmed) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val success = clearCustomUmountPaths()
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.custom_paths_cleared)
|
||||
)
|
||||
loadPaths()
|
||||
} else {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.operation_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.DeleteForever, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||
Text(stringResource(R.string.clear_custom_paths))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val success = applyUmountConfigToKernel()
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.config_applied)
|
||||
)
|
||||
} else {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.operation_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Filled.Check, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||
Text(stringResource(R.string.apply_config))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddDialog) {
|
||||
AddUmountPathDialog(
|
||||
onDismiss = { showAddDialog = false },
|
||||
onConfirm = { path, checkMnt, flags ->
|
||||
showAddDialog = false
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val success = addUmountPath(path, checkMnt, flags)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (success) {
|
||||
saveUmountConfig()
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.umount_path_added)
|
||||
)
|
||||
loadPaths()
|
||||
} else {
|
||||
snackBarHost.showSnackbar(
|
||||
context.getString(R.string.operation_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UmountPathCard(
|
||||
entry: UmountPathEntry,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(SPACING_LARGE),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Folder,
|
||||
contentDescription = null,
|
||||
tint = if (entry.isDefault)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(SPACING_LARGE))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = entry.path,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||
Text(
|
||||
text = buildString {
|
||||
append(context.getString(R.string.check_mount_type))
|
||||
append(": ")
|
||||
append(if (entry.checkMnt) context.getString(R.string.yes) else context.getString(R.string.no))
|
||||
append(" | ")
|
||||
append(context.getString(R.string.flags))
|
||||
append(": ")
|
||||
append(entry.flags.toUmountFlagName(context))
|
||||
if (entry.isDefault) {
|
||||
append(" | ")
|
||||
append(context.getString(R.string.default_entry))
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (!entry.isDefault) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (confirmDialog.awaitConfirm(
|
||||
title = context.getString(R.string.confirm_delete),
|
||||
content = context.getString(R.string.confirm_delete_umount_path, entry.path)
|
||||
) == ConfirmResult.Confirmed) {
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddUmountPathDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, Boolean, Int) -> Unit
|
||||
) {
|
||||
var path by rememberSaveable { mutableStateOf("") }
|
||||
var checkMnt by rememberSaveable { mutableStateOf(false) }
|
||||
var flags by rememberSaveable { mutableStateOf("-1") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.add_umount_path)) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = path,
|
||||
onValueChange = { path = it },
|
||||
label = { Text(stringResource(R.string.mount_path)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checkMnt,
|
||||
onCheckedChange = { checkMnt = it }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(SPACING_SMALL))
|
||||
Text(stringResource(R.string.check_mount_type_overlay))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||
|
||||
OutlinedTextField(
|
||||
value = flags,
|
||||
onValueChange = { flags = it },
|
||||
label = { Text(stringResource(R.string.umount_flags)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
supportingText = { Text(stringResource(R.string.umount_flags_hint)) }
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val flagsInt = flags.toIntOrNull() ?: -1
|
||||
onConfirm(path, checkMnt, flagsInt)
|
||||
},
|
||||
enabled = path.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseUmountPaths(output: String): List<UmountPathEntry> {
|
||||
val lines = output.lines().filter { it.isNotBlank() }
|
||||
if (lines.size < 2) return emptyList()
|
||||
|
||||
return lines.drop(2).mapNotNull { line ->
|
||||
val parts = line.trim().split(Regex("\\s+"))
|
||||
if (parts.size >= 4) {
|
||||
UmountPathEntry(
|
||||
path = parts[0],
|
||||
checkMnt = parts[1].equals("true", ignoreCase = true),
|
||||
flags = parts[2].toIntOrNull() ?: -1,
|
||||
isDefault = parts[3].equals("Yes", ignoreCase = true)
|
||||
)
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toUmountFlagName(context: android.content.Context): String {
|
||||
return when (this) {
|
||||
-1 -> context.getString(R.string.mnt_detach)
|
||||
else -> this.toString()
|
||||
}
|
||||
}
|
||||
@@ -665,3 +665,56 @@ fun readUidScannerFile(): Boolean {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun addUmountPath(path: String, checkMnt: Boolean, flags: Int): Boolean {
|
||||
val shell = getRootShell()
|
||||
val checkMntFlag = if (checkMnt) "--check-mnt" else ""
|
||||
val flagsArg = if (flags >= 0) "--flags $flags" else ""
|
||||
val cmd = "${getKsuDaemonPath()} umount add $path $checkMntFlag $flagsArg"
|
||||
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||
Log.i(TAG, "add umount path $path result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun removeUmountPath(path: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKsuDaemonPath()} umount remove $path"
|
||||
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||
Log.i(TAG, "remove umount path $path result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun listUmountPaths(): String {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKsuDaemonPath()} umount list"
|
||||
return try {
|
||||
runCmd(shell, cmd).trim()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to list umount paths", e)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCustomUmountPaths(): Boolean {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKsuDaemonPath()} umount clear-custom"
|
||||
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||
Log.i(TAG, "clear custom umount paths result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun saveUmountConfig(): Boolean {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKsuDaemonPath()} umount save"
|
||||
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||
Log.i(TAG, "save umount config result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun applyUmountConfigToKernel(): Boolean {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKsuDaemonPath()} umount apply"
|
||||
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||
Log.i(TAG, "apply umount config to kernel result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -729,4 +729,26 @@
|
||||
<string name="incompatible_kernel_msg">
|
||||
当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d)
|
||||
</string>
|
||||
<string name="umount_path_manager">Umount 路径管理</string>
|
||||
<string name="umount_path_manager_summary">管理内核卸载路径</string>
|
||||
<string name="umount_path_restart_notice">添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。</string>
|
||||
<string name="add_umount_path">添加 Umount 路径</string>
|
||||
<string name="mount_path">挂载路径</string>
|
||||
<string name="check_mount_type">检查挂载类型</string>
|
||||
<string name="check_mount_type_overlay">检查是否为 overlay 类型</string>
|
||||
<string name="umount_flags">卸载标志</string>
|
||||
<string name="umount_flags_hint">0=正常卸载, 8=MNT_DETACH, -1=自动</string>
|
||||
<string name="flags">标志</string>
|
||||
<string name="default_entry">默认条目</string>
|
||||
<string name="confirm_delete">确认删除</string>
|
||||
<string name="confirm_delete_umount_path">确定要删除路径 %s 吗?</string>
|
||||
<string name="umount_path_added">路径已添加,重启后生效</string>
|
||||
<string name="umount_path_removed">路径已删除,重启后生效</string>
|
||||
<string name="operation_failed">操作失败</string>
|
||||
<string name="confirm_action">确认操作</string>
|
||||
<string name="confirm_clear_custom_paths">确定要清除所有自定义路径吗?(默认路径将保留)</string>
|
||||
<string name="custom_paths_cleared">自定义路径已清除</string>
|
||||
<string name="clear_custom_paths">清除自定义</string>
|
||||
<string name="apply_config">应用配置</string>
|
||||
<string name="config_applied">配置已应用到内核</string>
|
||||
</resources>
|
||||
|
||||
@@ -737,4 +737,27 @@ Important Note:\n
|
||||
<string name="incompatible_kernel_msg">
|
||||
The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d)
|
||||
</string>
|
||||
<string name="umount_path_manager">Umount Path Management</string>
|
||||
<string name="umount_path_manager_summary">Manage kernel unmount paths</string>
|
||||
<string name="umount_path_restart_notice">A reboot is required for changes to take effect. The system will apply the new configuration on the next boot.</string>
|
||||
<string name="add_umount_path">Add Umount Path</string>
|
||||
<string name="mount_path">Mount Path</string>
|
||||
<string name="check_mount_type">Check Mount Type</string>
|
||||
<string name="check_mount_type_overlay">Check if it is an overlay type</string>
|
||||
<string name="umount_flags">Unmount Flags</string>
|
||||
<string name="umount_flags_hint">0=Normal unmount, 8=MNT_DETACH, -1=Auto</string>
|
||||
<string name="flags">Flags</string>
|
||||
<string name="default_entry">Default Entry</string>
|
||||
<string name="confirm_delete">Confirm Delete</string>
|
||||
<string name="confirm_delete_umount_path">Are you sure you want to delete the path %s?</string>
|
||||
<string name="umount_path_added">Path added, will take effect after reboot</string>
|
||||
<string name="umount_path_removed">Path removed, will take effect after reboot</string>
|
||||
<string name="operation_failed">Operation failed</string>
|
||||
<string name="confirm_action">Confirm Action</string>
|
||||
<string name="confirm_clear_custom_paths">Are you sure you want to clear all custom paths? (Default paths will be preserved)</string>
|
||||
<string name="custom_paths_cleared">Custom paths cleared</string>
|
||||
<string name="clear_custom_paths">Clear Custom Paths</string>
|
||||
<string name="apply_config">Apply Configuration</string>
|
||||
<string name="config_applied">Configuration applied to kernel</string>
|
||||
<string name="mnt_detach">MNT_DETACH</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user