Improve the ui and function of the anykernel3 flashing interface.

- Add self-selected brushwrite A/B slot (not perfect)

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
ShirkNeko
2025-04-27 18:01:45 +08:00
parent 7769a23f59
commit 85b4d11912
5 changed files with 583 additions and 143 deletions

View File

@@ -0,0 +1,372 @@
package com.sukisu.ultra.flash
import android.app.Activity
import android.content.Context
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import com.sukisu.ultra.R
import com.sukisu.ultra.utils.AssetsUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
data class FlashState(
val isFlashing: Boolean = false,
val isCompleted: Boolean = false,
val progress: Float = 0f,
val currentStep: String = "",
val logs: List<String> = emptyList(),
val error: String = ""
)
class HorizonKernelState {
private val _state = MutableStateFlow(FlashState())
val state: StateFlow<FlashState> = _state.asStateFlow()
fun updateProgress(progress: Float) {
_state.update { it.copy(progress = progress) }
}
fun updateStep(step: String) {
_state.update { it.copy(currentStep = step) }
}
fun addLog(log: String) {
_state.update {
it.copy(logs = it.logs + log)
}
}
fun setError(error: String) {
_state.update { it.copy(error = error) }
}
fun startFlashing() {
_state.update {
it.copy(
isFlashing = true,
isCompleted = false,
progress = 0f,
currentStep = "under preparation...",
logs = emptyList(),
error = ""
)
}
}
fun completeFlashing() {
_state.update { it.copy(isCompleted = true, progress = 1f) }
}
fun reset() {
_state.value = FlashState()
}
}
class HorizonKernelWorker(
private val context: Context,
private val state: HorizonKernelState,
private val slot: String? = null
) : Thread() {
var uri: Uri? = null
private lateinit var filePath: String
private lateinit var binaryPath: String
private var onFlashComplete: (() -> Unit)? = null
fun setOnFlashCompleteListener(listener: () -> Unit) {
onFlashComplete = listener
}
override fun run() {
state.startFlashing()
state.updateStep(context.getString(R.string.horizon_preparing))
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
try {
state.updateStep(context.getString(R.string.horizon_cleaning_files))
state.updateProgress(0.1f)
cleanup()
if (!rootAvailable()) {
state.setError(context.getString(R.string.root_required))
return
}
state.updateStep(context.getString(R.string.horizon_copying_files))
state.updateProgress(0.2f)
copy()
if (!File(filePath).exists()) {
state.setError(context.getString(R.string.horizon_copy_failed))
return
}
state.updateStep(context.getString(R.string.horizon_extracting_tool))
state.updateProgress(0.4f)
getBinary()
state.updateStep(context.getString(R.string.horizon_patching_script))
state.updateProgress(0.6f)
patch()
state.updateStep(context.getString(R.string.horizon_flashing))
state.updateProgress(0.7f)
flash()
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
state.completeFlashing()
(context as? Activity)?.runOnUiThread {
onFlashComplete?.invoke()
}
} catch (e: Exception) {
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
}
}
private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
}
private fun copy() {
uri?.let { safeUri ->
context.contentResolver.openInputStream(safeUri)?.use { input ->
FileOutputStream(File(filePath)).use { output ->
input.copyTo(output)
}
}
}
}
private fun getBinary() {
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
if (!File(binaryPath).exists()) {
throw IOException("Failed to extract update-binary")
}
}
private fun patch() {
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
}
private fun flash() {
val process = ProcessBuilder("su")
.redirectErrorStream(true)
.start()
try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
// 写入槽位信息到临时文件
slot?.let { selectedSlot ->
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
}
// 构建刷写命令
val flashCommand = buildString {
append("sh $binaryPath 3 1 \"$filePath\"")
if (slot != null) {
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
}
append(" && touch ${context.filesDir.absolutePath}/done\n")
}
writer.write(flashCommand)
writer.write("exit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.lineSequence().forEach { line ->
if (line.startsWith("ui_print")) {
val logMessage = line.removePrefix("ui_print").trim()
state.addLog(logMessage)
when {
logMessage.contains("extracting", ignoreCase = true) -> {
state.updateProgress(0.75f)
}
logMessage.contains("installing", ignoreCase = true) -> {
state.updateProgress(0.85f)
}
logMessage.contains("complete", ignoreCase = true) -> {
state.updateProgress(0.95f)
}
}
}
}
}
} finally {
process.destroy()
}
if (!File("${context.filesDir.absolutePath}/done").exists()) {
throw IOException("Flash failed")
}
}
private fun runCommand(su: Boolean, cmd: String): Int {
val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true)
.start()
return try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.waitFor()
} finally {
process.destroy()
}
}
private fun rootAvailable(): Boolean {
return try {
val process = Runtime.getRuntime().exec("su -c id")
val exitValue = process.waitFor()
exitValue == 0
} catch (e: Exception) {
false
}
}
}
@Composable
fun HorizonKernelFlashProgress(state: FlashState) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.horizon_flash_title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
progress = { state.progress },
)
Text(
text = state.currentStep,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 4.dp)
)
if (state.logs.isNotEmpty()) {
Text(
text = stringResource(id = R.string.horizon_logs_label),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.align(Alignment.Start)
.padding(top = 8.dp, bottom = 4.dp)
)
Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 150.dp)
.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp,
shape = MaterialTheme.shapes.small
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.verticalScroll(rememberScrollState())
) {
state.logs.forEach { log ->
Text(
text = log,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(vertical = 2.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
}
}
if (state.error.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = state.error,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
} else if (state.isCompleted) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(id = R.string.horizon_flash_complete),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
/**
* 槽位选择对话框组件
* 用于HorizonKernel刷写时选择目标槽位
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SlotSelectionDialog(
show: Boolean,
onDismiss: () -> Unit,
onSlotSelected: (String) -> Unit
) {
if (show) {
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.select_slot_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.select_slot_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Button(
onClick = { onSlotSelected("a") },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
) {
Text(
text = stringResource(id = R.string.slot_a),
style = MaterialTheme.typography.labelLarge
)
}
Button(
onClick = { onSlotSelected("b") },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Text(
text = stringResource(id = R.string.slot_b),
style = MaterialTheme.typography.labelLarge
)
}
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.medium,
tonalElevation = getCardElevation()
)
}
}

View File

@@ -1,13 +1,17 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -28,26 +32,24 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import com.maxkeppeler.sheets.list.models.ListOption import com.maxkeppeler.sheets.list.models.ListOption
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.DialogHandle import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
import com.sukisu.ultra.flash.HorizonKernelState
import com.sukisu.ultra.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.theme.ThemeConfig import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.R
import com.sukisu.ultra.utils.AssetsUtil
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
/** /**
* @author weishu * @author weishu
@@ -60,8 +62,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
var installMethod by remember { mutableStateOf<InstallMethod?>(null) } var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) } var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
val context = LocalContext.current val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) } var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val horizonKernelState = remember { HorizonKernelState() }
val flashState by horizonKernelState.state.collectAsState()
val summary = stringResource(R.string.horizon_kernel_summary)
val onFlashComplete = { val onFlashComplete = {
showRebootDialog = true showRebootDialog = true
@@ -79,7 +85,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
writer.write("svc power reboot\n") writer.write("svc power reboot\n")
writer.write("exit\n") writer.write("exit\n")
} }
} catch (e: Exception) { } catch (_: Exception) {
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
} }
} }
@@ -91,7 +97,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
when (method) { when (method) {
is InstallMethod.HorizonKernel -> { is InstallMethod.HorizonKernel -> {
method.uri?.let { uri -> method.uri?.let { uri ->
val worker = HorizonKernelWorker(context) val worker = HorizonKernelWorker(
context = context,
state = horizonKernelState,
slot = method.slot
)
worker.uri = uri worker.uri = uri
worker.setOnFlashCompleteListener(onFlashComplete) worker.setOnFlashCompleteListener(onFlashComplete)
worker.start() worker.start()
@@ -110,6 +120,22 @@ fun InstallScreen(navigator: DestinationsNavigator) {
Unit Unit
} }
// 槽位选择
SlotSelectionDialog(
show = showSlotSelectionDialog,
onDismiss = { showSlotSelectionDialog = false },
onSlotSelected = { slot ->
showSlotSelectionDialog = false
val horizonMethod = InstallMethod.HorizonKernel(
uri = tempKernelUri,
slot = slot,
summary = summary
)
installMethod = horizonMethod
onInstall()
}
)
val currentKmi by produceState(initialValue = "") { val currentKmi by produceState(initialValue = "") {
value = getCurrentKmi() value = getCurrentKmi()
} }
@@ -165,8 +191,24 @@ fun InstallScreen(navigator: DestinationsNavigator) {
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
SelectInstallMethod { method -> SelectInstallMethod(
installMethod = method onSelected = { method ->
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) {
tempKernelUri = method.uri
showSlotSelectionDialog = true
} else {
installMethod = method
}
horizonKernelState.reset()
}
)
AnimatedVisibility(
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
HorizonKernelFlashProgress(flashState)
} }
Column( Column(
@@ -182,9 +224,20 @@ fun InstallScreen(navigator: DestinationsNavigator) {
) )
) )
} }
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
if (method.slot != null) {
Text(
stringResource(
id = R.string.selected_slot,
if (method.slot == "a") stringResource(id = R.string.slot_a)
else stringResource(id = R.string.slot_b)
)
)
}
}
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null, enabled = installMethod != null && !flashState.isFlashing,
onClick = onClickNext onClick = onClickNext
) { ) {
Text( Text(
@@ -197,7 +250,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} }
} }
@Composable @Composable
private fun RebootDialog( private fun RebootDialog(
show: Boolean, show: Boolean,
@@ -223,133 +275,6 @@ private fun RebootDialog(
} }
} }
private class HorizonKernelWorker(private val context: Context) : Thread() {
var uri: Uri? = null
private lateinit var filePath: String
private lateinit var binaryPath: String
private var onFlashComplete: (() -> Unit)? = null
fun setOnFlashCompleteListener(listener: () -> Unit) {
onFlashComplete = listener
}
override fun run() {
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
try {
cleanup()
if (!rootAvailable()) {
showError(context.getString(R.string.root_required))
return
}
copy()
if (!File(filePath).exists()) {
showError(context.getString(R.string.copy_failed))
return
}
getBinary()
patch()
flash()
(context as? Activity)?.runOnUiThread {
onFlashComplete?.invoke()
}
} catch (e: Exception) {
showError(e.message ?: context.getString(R.string.unknown_error))
}
}
private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
}
private fun copy() {
uri?.let { safeUri ->
context.contentResolver.openInputStream(safeUri)?.use { input ->
FileOutputStream(File(filePath)).use { output ->
input.copyTo(output)
}
}
}
}
private fun getBinary() {
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
if (!File(binaryPath).exists()) {
throw IOException("Failed to extract update-binary")
}
}
private fun patch() {
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
}
private fun flash() {
val process = ProcessBuilder("su")
.redirectErrorStream(true)
.start()
try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.lineSequence().forEach { line ->
if (line.startsWith("ui_print")) {
showLog(line.removePrefix("ui_print"))
}
}
}
} finally {
process.destroy()
}
if (!File("${context.filesDir.absolutePath}/done").exists()) {
throw IOException("Flash failed")
}
}
private fun runCommand(su: Boolean, cmd: String): Int {
val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true)
.start()
return try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.waitFor()
} finally {
process.destroy()
}
}
private fun showError(message: String) {
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showLog(message: String) {
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
}
sealed class InstallMethod { sealed class InstallMethod {
data class SelectFile( data class SelectFile(
val uri: Uri? = null, val uri: Uri? = null,
@@ -369,6 +294,7 @@ sealed class InstallMethod {
data class HorizonKernel( data class HorizonKernel(
val uri: Uri? = null, val uri: Uri? = null,
val slot: String? = null,
@StringRes override val label: Int = R.string.horizon_kernel, @StringRes override val label: Int = R.string.horizon_kernel,
override val summary: String? = null override val summary: String? = null
) : InstallMethod() ) : InstallMethod()
@@ -402,7 +328,6 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) } var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) } var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult( val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { ) {

View File

@@ -266,4 +266,25 @@
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string> <string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
<string name="background_image_error">无法加载图片</string> <string name="background_image_error">无法加载图片</string>
<string name="reprovision">重置</string> <string name="reprovision">重置</string>
<!-- Anykernel3 Kernel刷写进度相关 -->
<string name="horizon_flash_title">刷写Anykernel3</string>
<string name="horizon_logs_label">日志:</string>
<string name="horizon_flash_complete">刷写完成</string>
<!-- 刷写状态相关 -->
<string name="horizon_preparing">准备中…</string>
<string name="horizon_cleaning_files">清理文件…</string>
<string name="horizon_copying_files">复制文件…</string>
<string name="horizon_extracting_tool">提取刷写工具…</string>
<string name="horizon_patching_script">修补刷写脚本…</string>
<string name="horizon_flashing">刷写内核中…</string>
<string name="horizon_flash_complete_status">刷写完成</string>
<!-- 槽位选择相关字符串 -->
<string name="select_slot_title">选择刷写槽位</string>
<string name="select_slot_description">请选择要刷写HorizonKernel的目标槽位</string>
<string name="slot_a">A槽位</string>
<string name="slot_b">B槽位</string>
<string name="selected_slot">已选择槽位: %1$s</string>
<!-- 错误信息 -->
<string name="horizon_copy_failed">复制失败</string>
<string name="horizon_unknown_error">未知错误</string>
</resources> </resources>

View File

@@ -270,4 +270,25 @@
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string> <string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="background_image_error">Could not load image</string> <string name="background_image_error">Could not load image</string>
<string name="reprovision">Reprovision</string> <string name="reprovision">Reprovision</string>
<!-- Anykernel3 Kernel Flash Progress Related -->
<string name="horizon_flash_title">Anykernel3 Flashing</string>
<string name="horizon_logs_label">Logs:</string>
<string name="horizon_flash_complete">Flash Complete</string>
<!-- Flash Status Related -->
<string name="horizon_preparing">Preparing…</string>
<string name="horizon_cleaning_files">Cleaning files…</string>
<string name="horizon_copying_files">Copying files…</string>
<string name="horizon_extracting_tool">Extracting flash tool…</string>
<string name="horizon_patching_script">Patching flash script…</string>
<string name="horizon_flashing">Flashing kernel…</string>
<string name="horizon_flash_complete_status">Flash completed</string>
<!-- Slot selection related strings -->
<string name="select_slot_title">Select Flash Slot</string>
<string name="select_slot_description">Please select the target slot for flashing HorizonKernel</string>
<string name="slot_a">Slot A</string>
<string name="slot_b">Slot B</string>
<string name="selected_slot">Selected slot: %1$s</string>
<!-- Error Messages -->
<string name="horizon_copy_failed">Copy failed</string>
<string name="horizon_unknown_error">Unknown error</string>
</resources> </resources>