From 85b4d11912b66c3702959caa2ec6a099b068129a Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Sun, 27 Apr 2025 18:01:45 +0800 Subject: [PATCH] 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> --- .../com/sukisu/ultra/flash/KernelFlash.kt | 372 ++++++++++++++++++ .../ultra/ui/component/SlotSelectionDialog.kt | 101 +++++ .../com/sukisu/ultra/ui/screen/Install.kt | 211 ++++------ .../src/main/res/values-zh-rCN/strings.xml | 21 + manager/app/src/main/res/values/strings.xml | 21 + 5 files changed, 583 insertions(+), 143 deletions(-) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt diff --git a/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt b/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt new file mode 100644 index 00000000..c26dca01 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt @@ -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 = emptyList(), + val error: String = "" +) + +class HorizonKernelState { + private val _state = MutableStateFlow(FlashState()) + val state: StateFlow = _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 + ) + } + } + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt new file mode 100644 index 00000000..50834f31 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt @@ -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() + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 80fdaa6c..c5b8cbec 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -1,13 +1,17 @@ package com.sukisu.ultra.ui.screen import android.app.Activity -import android.content.Context import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts 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.clickable 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.documentfile.provider.DocumentFile import com.maxkeppeler.sheets.list.models.ListOption import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import com.sukisu.ultra.R 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.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.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation 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 @@ -60,8 +62,12 @@ fun InstallScreen(navigator: DestinationsNavigator) { var installMethod by remember { mutableStateOf(null) } var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } val context = LocalContext.current - var showRebootDialog by remember { mutableStateOf(false) } + var showSlotSelectionDialog by remember { mutableStateOf(false) } + var tempKernelUri by remember { mutableStateOf(null) } + val horizonKernelState = remember { HorizonKernelState() } + val flashState by horizonKernelState.state.collectAsState() + val summary = stringResource(R.string.horizon_kernel_summary) val onFlashComplete = { showRebootDialog = true @@ -79,7 +85,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { writer.write("svc power reboot\n") writer.write("exit\n") } - } catch (e: Exception) { + } catch (_: Exception) { Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show() } } @@ -91,7 +97,11 @@ fun InstallScreen(navigator: DestinationsNavigator) { when (method) { is InstallMethod.HorizonKernel -> { method.uri?.let { uri -> - val worker = HorizonKernelWorker(context) + val worker = HorizonKernelWorker( + context = context, + state = horizonKernelState, + slot = method.slot + ) worker.uri = uri worker.setOnFlashCompleteListener(onFlashComplete) worker.start() @@ -110,6 +120,22 @@ fun InstallScreen(navigator: DestinationsNavigator) { 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 = "") { value = getCurrentKmi() } @@ -165,8 +191,24 @@ fun InstallScreen(navigator: DestinationsNavigator) { .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) ) { - SelectInstallMethod { method -> - installMethod = method + SelectInstallMethod( + 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( @@ -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( modifier = Modifier.fillMaxWidth(), - enabled = installMethod != null, + enabled = installMethod != null && !flashState.isFlashing, onClick = onClickNext ) { Text( @@ -197,7 +250,6 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } - @Composable private fun RebootDialog( 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 { data class SelectFile( val uri: Uri? = null, @@ -369,6 +294,7 @@ sealed class InstallMethod { data class HorizonKernel( val uri: Uri? = null, + val slot: String? = null, @StringRes override val label: Int = R.string.horizon_kernel, override val summary: String? = null ) : InstallMethod() @@ -402,7 +328,6 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { var selectedOption by remember { mutableStateOf(null) } var currentSelectingMethod by remember { mutableStateOf(null) } - val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { 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 7442e2ae..7e161b32 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -266,4 +266,25 @@ 使用双指缩放图片,单指拖动调整位置 无法加载图片 重置 + + 刷写Anykernel3 + 日志: + 刷写完成 + + 准备中… + 清理文件… + 复制文件… + 提取刷写工具… + 修补刷写脚本… + 刷写内核中… + 刷写完成 + + 选择刷写槽位 + 请选择要刷写HorizonKernel的目标槽位 + A槽位 + B槽位 + 已选择槽位: %1$s + + 复制失败 + 未知错误 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 894ed0f7..e402b3a3 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -270,4 +270,25 @@ Use two fingers to zoom the image, and one finger to drag it to adjust the position Could not load image Reprovision + + Anykernel3 Flashing + Logs: + Flash Complete + + Preparing… + Cleaning files… + Copying files… + Extracting flash tool… + Patching flash script… + Flashing kernel… + Flash completed + + Select Flash Slot + Please select the target slot for flashing HorizonKernel + Slot A + Slot B + Selected slot: %1$s + + Copy failed + Unknown error