manager: install: add choose partition support

manager: fix KsuCli cmd

userspace: reuse choose_boot_device

- manager: simplify find boot image

Co-authored-by: weishu <twsxtd@gmail.com>
Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
ShirkNeko
2025-11-11 15:16:11 +08:00
parent 8f49898155
commit 5ce6c210c4
10 changed files with 604 additions and 548 deletions

View File

@@ -0,0 +1,250 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SuperDropdown(
items: List<String>,
selectedIndex: Int,
title: String,
summary: String? = null,
icon: ImageVector? = null,
enabled: Boolean = true,
showValue: Boolean = true,
maxHeight: Dp? = 400.dp,
colors: SuperDropdownColors = SuperDropdownDefaults.colors(),
leftAction: (@Composable () -> Unit)? = null,
onSelectedIndexChange: (Int) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
val selectedItemText = items.getOrNull(selectedIndex) ?: ""
val itemsNotEmpty = items.isNotEmpty()
val actualEnabled = enabled && itemsNotEmpty
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = actualEnabled) { showDialog = true }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.Top
) {
if (leftAction != null) {
leftAction()
} else if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor
)
if (summary != null) {
Spacer(modifier = Modifier.height(3.dp))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor
)
}
if (showValue && itemsNotEmpty) {
Spacer(modifier = Modifier.height(3.dp))
Text(
text = selectedItemText,
style = MaterialTheme.typography.bodyMedium,
color = if (actualEnabled) colors.valueColor else colors.disabledValueColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor,
modifier = Modifier.size(24.dp)
)
}
if (showDialog && itemsNotEmpty) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall
)
},
text = {
val dialogMaxHeight = maxHeight ?: 400.dp
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = dialogMaxHeight),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(items.size) { index ->
DropdownItem(
text = items[index],
isSelected = selectedIndex == index,
colors = colors,
onClick = {
onSelectedIndexChange(index)
showDialog = false
}
)
}
}
},
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
containerColor = colors.dialogBackgroundColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
}
}
@Composable
private fun DropdownItem(
text: String,
isSelected: Boolean,
colors: SuperDropdownColors,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) {
colors.selectedBackgroundColor
} else {
Color.Transparent
}
val contentColor = if (isSelected) {
colors.selectedContentColor
} else {
colors.contentColor
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor)
.clickable(onClick = onClick)
.padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = isSelected,
onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = colors.selectedContentColor,
unselectedColor = colors.contentColor
)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = contentColor,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = colors.selectedContentColor,
modifier = Modifier.size(20.dp)
)
}
}
}
@Immutable
data class SuperDropdownColors(
val titleColor: Color,
val summaryColor: Color,
val valueColor: Color,
val iconColor: Color,
val arrowColor: Color,
val disabledTitleColor: Color,
val disabledSummaryColor: Color,
val disabledValueColor: Color,
val disabledIconColor: Color,
val disabledArrowColor: Color,
val dialogBackgroundColor: Color,
val contentColor: Color,
val selectedContentColor: Color,
val selectedBackgroundColor: Color
)
object SuperDropdownDefaults {
@Composable
fun colors(
titleColor: Color = MaterialTheme.colorScheme.onSurface,
summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
iconColor: Color = MaterialTheme.colorScheme.primary,
arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
selectedContentColor: Color = MaterialTheme.colorScheme.primary,
selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
): SuperDropdownColors {
return SuperDropdownColors(
titleColor = titleColor,
summaryColor = summaryColor,
valueColor = valueColor,
iconColor = iconColor,
arrowColor = arrowColor,
disabledTitleColor = disabledTitleColor,
disabledSummaryColor = disabledSummaryColor,
disabledValueColor = disabledValueColor,
disabledIconColor = disabledIconColor,
disabledArrowColor = disabledArrowColor,
dialogBackgroundColor = dialogBackgroundColor,
contentColor = contentColor,
selectedContentColor = selectedContentColor,
selectedBackgroundColor = selectedBackgroundColor
)
}
}

View File

@@ -707,7 +707,7 @@ suspend fun getModuleNameFromUri(context: Context, uri: Uri): String {
@Parcelize
sealed class FlashIt : Parcelable {
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt()
data class FlashModule(val uri: Uri) : FlashIt()
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新
@@ -736,6 +736,7 @@ fun flashIt(
flashIt.boot,
flashIt.lkm,
flashIt.ota,
flashIt.partition,
onFinish,
onStdout,
onStderr

View File

@@ -21,6 +21,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Input
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.*
@@ -51,7 +52,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ui.component.DialogHandle
import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.SuperDropdown
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig
@@ -60,6 +61,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog
/**
* @author ShirkNeko
@@ -87,9 +89,12 @@ fun InstallScreen(
var showSlotSelectionDialog by remember { mutableStateOf(false) }
var showKpmPatchDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val summary = stringResource(R.string.horizon_kernel_summary)
// 处理预选的内核文件
@@ -103,6 +108,7 @@ fun InstallScreen(
)
installMethod = horizonMethod
tempKernelUri = preselectedUri
if (isAbDevice) {
showSlotSelectionDialog = true
} else {
@@ -133,6 +139,10 @@ fun InstallScreen(
)
}
var partitionSelectionIndex by remember { mutableIntStateOf(0) }
var partitionsState by remember { mutableStateOf<List<String>>(emptyList()) }
var hasCustomSelected by remember { mutableStateOf(false) }
val onInstall = {
installMethod?.let { method ->
when (method) {
@@ -149,10 +159,13 @@ fun InstallScreen(
}
}
else -> {
val isOta = method is InstallMethod.DirectInstallToInactiveSlot
val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex)
val flashIt = FlashIt.FlashBoot(
boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot
ota = isOta,
partition = partitionSelection
)
navigator.navigate(FlashScreenDestination(flashIt))
}
@@ -173,6 +186,7 @@ fun InstallScreen(
summary = summary
)
installMethod = horizonMethod
if (preselectedKernelUri != null) {
showKpmPatchDialog = true
}
@@ -274,11 +288,72 @@ fun InstallScreen(
selectedMethod = installMethod
)
// 选择LKM直接安装分区
AnimatedVisibility(
visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
) {
val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot
val suffix = produceState(initialValue = "", isOta) {
value = getSlotSuffix(isOta)
}.value
val partitions = produceState(initialValue = emptyList()) {
value = getAvailablePartitions()
}.value
val defaultPartition = produceState(initialValue = "") {
value = getDefaultPartition()
}.value
partitionsState = partitions
val displayPartitions = partitions.map { name ->
if (defaultPartition == name) "$name (default)" else name
}
val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0
if (!hasCustomSelected) partitionSelectionIndex = defaultIndex
SuperDropdown(
items = displayPartitions,
selectedIndex = partitionSelectionIndex,
title = "${stringResource(R.string.install_select_partition)} (${suffix})",
onSelectedIndexChange = { index ->
hasCustomSelected = true
partitionSelectionIndex = index
},
leftAction = {
Icon(
Icons.Default.Edit,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 16.dp),
contentDescription = null
)
}
)
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 使用本地的LKM文件
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
@@ -311,7 +386,8 @@ fun InstallScreen(
.clickable { onLkmUpload() }
)
}
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
if (method.slot != null) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
@@ -319,12 +395,6 @@ fun InstallScreen(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = stringResource(
@@ -346,12 +416,6 @@ fun InstallScreen(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = when (kpmPatchOption) {
@@ -496,15 +560,15 @@ private fun SelectInstallMethod(
selectedMethod: InstallMethod? = null
) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val defaultPartitionName = produceState(initialValue = "boot") {
value = getDefaultPartition()
}.value
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
val selectFileTip = stringResource(
id = R.string.select_file_tip,
if (isInitBoot()) {
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
} else {
"boot"
}
id = R.string.select_file_tip, defaultPartitionName
)
val radioOptions = mutableListOf<InstallMethod>(
@@ -601,7 +665,6 @@ private fun SelectInstallMethod(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
@@ -640,7 +703,7 @@ private fun SelectInstallMethod(
bottom = 16.dp
)
) {
radioOptions.take(3).forEach { option ->
radioOptions.filter { it !is InstallMethod.HorizonKernel }.forEach { option ->
val interactionSource = remember { MutableInteractionSource() }
Surface(
color = if (option.javaClass == selectedOption?.javaClass)
@@ -708,7 +771,6 @@ private fun SelectInstallMethod(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(

View File

@@ -150,7 +150,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
}
SettingDropdown(
SuperDropdown(
icon = Icons.Rounded.EnhancedEncryption,
title = stringResource(id = R.string.settings_enable_enhanced_security),
summary = stringResource(id = R.string.settings_enable_enhanced_security_summary),
@@ -193,7 +193,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
}
SettingDropdown(
SuperDropdown(
icon = Icons.Rounded.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary),
@@ -236,7 +236,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
}
SettingDropdown(
SuperDropdown(
icon = Icons.Rounded.RemoveCircle,
title = stringResource(id = R.string.settings_disable_kernel_umount),
summary = stringResource(id = R.string.settings_disable_kernel_umount_summary),
@@ -1077,103 +1077,3 @@ private fun UidScannerSection(
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingDropdown(
icon: ImageVector,
title: String,
summary: String,
items: List<String>,
selectedIndex: Int,
leftAction: (@Composable () -> Unit)? = null,
onSelectedIndexChange: (Int) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
val selectedItemText = items.getOrNull(selectedIndex) ?: ""
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { showDialog = true }
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
verticalAlignment = Alignment.Top
) {
if (leftAction != null) {
leftAction()
} else {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = SPACING_LARGE)
.size(24.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = selectedItemText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text(text = title) },
text = {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items.size) { index ->
val item = items[index]
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onSelectedIndexChange(index)
showDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedIndex == index,
onClick = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(text = item)
}
}
}
},
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(text = stringResource(android.R.string.cancel))
}
}
)
}
}

View File

@@ -1573,7 +1573,9 @@ private fun BasicSettingsContent(
onEnableAvcLogSpoofingChange: (Boolean) -> Unit
) {
var scriptLocationExpanded by remember { mutableStateOf(false) }
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val isSusVersion159 = isSusVersion159()
Column(
@@ -2063,7 +2065,9 @@ private fun SlotInfoDialog(
onUseUname: (String) -> Unit,
onUseBuildTime: (String) -> Unit
) {
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
if (showDialog && isAbDevice) {
AlertDialog(

View File

@@ -255,6 +255,7 @@ fun installBoot(
bootUri: Uri?,
lkm: LkmSelection,
ota: Boolean,
partition: String?,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit,
@@ -314,6 +315,10 @@ fun installBoot(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
cmd += " -o $downloadsDir"
partition?.let { part ->
cmd += " --partition $part"
}
val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
Log.i("KernelSU", "install boot result: ${result.isSuccess}")
@@ -344,18 +349,6 @@ fun rootAvailable(): Boolean {
return shell.isRoot
}
fun isAbDevice(): Boolean {
val shell = getRootShell()
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
}
fun isInitBoot(): Boolean {
val shell = getRootShell();
if (shell.isRoot) {
return SuFile("/dev/block/by-name/init_boot").exists() || SuFile("/dev/block/by-name/init_boot_a").exists()
}
return !Os.uname().release.contains("android12-")
}
suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
@@ -365,7 +358,36 @@ suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info supported-kmi"
val cmd = "boot-info supported-kmis"
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() }
}
suspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info is-ab-device"
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim().toBoolean()
}
suspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info default-partition"
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim()
}
suspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = if (ota) {
"boot-info slot-suffix --ota"
} else {
"boot-info slot-suffix"
}
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim()
}
suspend fun getAvailablePartitions(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info available-partitions"
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() }
}

View File

@@ -108,8 +108,9 @@
<string name="direct_install">直接安装(推荐)</string>
<string name="select_file">选择一个需要修补的镜像</string>
<string name="install_inactive_slot">安装到未使用的槽位OTA 后)</string>
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认</string>
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。</string>
<string name="install_next">下一步</string>
<string name="install_select_partition">选择分区</string>
<string name="install_upload_lkm_file">使用本地 LKM 文件</string>
<string name="install_only_support_ko_file">仅支持选择 .ko 文件</string>
<string name="select_file_tip">建议选择 %1$s 分区镜像</string>

View File

@@ -112,6 +112,7 @@
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
<string name="install_next">Next</string>
<string name="install_select_partition">Select partition</string>
<string name="install_upload_lkm_file">Use local LKM file</string>
<string name="install_only_support_ko_file">Only .ko files are supported</string>
<string name="select_file_tip">%1$s partition image is recommended</string>