From d5946047a11b718f80e7083b9b374664d66397a0 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Mon, 19 May 2025 16:49:37 +0800 Subject: [PATCH] manger: update flash style add instructions to make it easier to understand --- .../java/com/sukisu/ultra/ui/screen/Flash.kt | 439 +++++++++++++++--- .../src/main/res/values-zh-rCN/strings.xml | 4 + manager/app/src/main/res/values/strings.xml | 4 + 3 files changed, 378 insertions(+), 69 deletions(-) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index 9b63e86d..46d504ca 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -3,28 +3,40 @@ package com.sukisu.ultra.ui.screen import android.net.Uri import android.os.Environment import android.os.Parcelable +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background 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.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Save 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.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers @@ -38,6 +50,7 @@ import com.sukisu.ultra.ui.theme.CardConfig import java.io.File import java.text.SimpleDateFormat import java.util.* +import java.util.zip.ZipInputStream enum class FlashingStatus { FLASHING, @@ -47,14 +60,47 @@ enum class FlashingStatus { private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING) +// 添加模块安装状态跟踪 +data class ModuleInstallStatus( + val totalModules: Int = 0, + val currentModule: Int = 0, + val currentModuleName: String = "", + val failedModules: MutableList = mutableListOf() +) + +private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus()) + fun setFlashingStatus(status: FlashingStatus) { currentFlashingStatus.value = status } +fun updateModuleInstallStatus( + totalModules: Int? = null, + currentModule: Int? = null, + currentModuleName: String? = null, + failedModule: String? = null +) { + val current = moduleInstallStatus.value + moduleInstallStatus.value = current.copy( + totalModules = totalModules ?: current.totalModules, + currentModule = currentModule ?: current.currentModule, + currentModuleName = currentModuleName ?: current.currentModuleName + ) + + if (failedModule != null) { + val updatedFailedModules = current.failedModules.toMutableList() + updatedFailedModules.add(failedModule) + moduleInstallStatus.value = moduleInstallStatus.value.copy( + failedModules = updatedFailedModules + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable @Destination fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { + val context = LocalContext.current var text by rememberSaveable { mutableStateOf("") } var tempText: String val logContent = rememberSaveable { StringBuilder() } @@ -65,16 +111,56 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { val scrollState = rememberScrollState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val errorCodeString = stringResource(R.string.error_code) + val checkLogString = stringResource(R.string.check_log) + val logSavedString = stringResource(R.string.log_saved) + val installingModuleString = stringResource(R.string.installing_module) + + // 当前模块安装状态 + val currentStatus = moduleInstallStatus.value + + // 重置状态 + LaunchedEffect(flashIt) { + if (flashIt is FlashIt.FlashModules && flashIt.currentIndex == 0) { + moduleInstallStatus.value = ModuleInstallStatus( + totalModules = flashIt.uris.size, + currentModule = 1 + ) + } + } + LaunchedEffect(Unit) { if (text.isNotEmpty()) { return@LaunchedEffect } withContext(Dispatchers.IO) { setFlashingStatus(FlashingStatus.FLASHING) - flashIt(flashIt, onFinish = { showReboot, code -> + + if (flashIt is FlashIt.FlashModules) { + try { + val currentUri = flashIt.uris[flashIt.currentIndex] + val moduleName = getModuleNameFromUri(context, currentUri) + updateModuleInstallStatus( + currentModuleName = moduleName + ) + text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName) + logContent.append(text).append("\n") + } catch (_: Exception) { + text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module") + logContent.append(text).append("\n") + } + } + + flashIt(context, flashIt, onFinish = { showReboot, code -> if (code != 0) { - text += "Error: exit code = $code.\nPlease save and check the log.\n" + text += "$errorCodeString $code.\n$checkLogString\n" setFlashingStatus(FlashingStatus.FAILED) + + if (flashIt is FlashIt.FlashModules) { + updateModuleInstallStatus( + failedModule = moduleInstallStatus.value.currentModuleName + ) + } } else { setFlashingStatus(FlashingStatus.SUCCESS) } @@ -106,12 +192,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } } + BackHandler(enabled = true) { + if (currentFlashingStatus.value != FlashingStatus.FLASHING) { + navigator.navigate(ModuleScreenDestination) { + } + } + } + Scaffold( topBar = { TopBar( currentFlashingStatus.value, - onBack = dropUnlessResumed { - navigator.popBackStack() + currentStatus, + navigator = navigator, + onBack = { + if (currentFlashingStatus.value != FlashingStatus.FLASHING) { + navigator.navigate(ModuleScreenDestination) { + } + } }, onSave = { scope.launch { @@ -122,7 +220,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { "KernelSU_install_log_${date}.log" ) file.writeText(logContent.toString()) - snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + snackBarHost.showSnackbar(logSavedString.format(file.absolutePath)) } }, scrollBehavior = scrollBehavior @@ -130,8 +228,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { }, floatingActionButton = { if (showFloatAction) { - val cardColor = MaterialTheme.colorScheme.secondaryContainer - val reboot = stringResource(id = R.string.reboot) ExtendedFloatingActionButton( onClick = { scope.launch { @@ -140,36 +236,285 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } } }, - icon = { Icon(Icons.Filled.Refresh, reboot) }, - text = { Text(text = reboot) }, - containerColor = cardColor.copy(alpha = 1f), - contentColor = MaterialTheme.colorScheme.onSecondaryContainer + icon = { + Icon( + Icons.Filled.Refresh, + contentDescription = stringResource(id = R.string.reboot) + ) + }, + text = { + Text(text = stringResource(id = R.string.reboot)) + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + expanded = true ) } }, snackbarHost = { SnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } + Column( modifier = Modifier .fillMaxSize(1f) .padding(innerPadding) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(scrollState), + .nestedScroll(scrollBehavior.nestedScrollConnection), ) { - LaunchedEffect(text) { - scrollState.animateScrollTo(scrollState.maxValue) + if (flashIt is FlashIt.FlashModules) { + ModuleInstallProgressBar( + currentIndex = flashIt.currentIndex + 1, + totalCount = flashIt.uris.size, + currentModuleName = currentStatus.currentModuleName, + status = currentFlashingStatus.value, + failedModules = currentStatus.failedModules + ) + + Spacer(modifier = Modifier.height(8.dp)) } - Text( - modifier = Modifier.padding(8.dp), - text = text, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = FontFamily.Monospace, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScroll(scrollState) + ) { + LaunchedEffect(text) { + scrollState.animateScrollTo(scrollState.maxValue) + } + Text( + modifier = Modifier.padding(16.dp), + text = text, + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } +} + +// 显示模块安装进度条和状态 +@Composable +fun ModuleInstallProgressBar( + currentIndex: Int, + totalCount: Int, + currentModuleName: String, + status: FlashingStatus, + failedModules: List +) { + val progressColor = when(status) { + FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary + FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary + FlashingStatus.FAILED -> MaterialTheme.colorScheme.error + } + + val progress = animateFloatAsState( + targetValue = currentIndex.toFloat() / totalCount.toFloat(), + label = "InstallProgress" + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // 模块名称和进度 + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = if (currentModuleName.isNotEmpty()) currentModuleName else stringResource(R.string.module), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Text( + text = "$currentIndex/$totalCount", + style = MaterialTheme.typography.titleMedium + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 进度条 + LinearProgressIndicator( + progress = { progress.value }, + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + color = progressColor, + trackColor = MaterialTheme.colorScheme.surfaceVariant ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 失败模块列表 + AnimatedVisibility( + visible = failedModules.isNotEmpty(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = stringResource(R.string.module_failed_count, failedModules.size), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // 失败模块列表 + Column( + modifier = Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f), + shape = MaterialTheme.shapes.small + ) + .padding(8.dp) + ) { + failedModules.forEach { moduleName -> + Text( + text = "• $moduleName", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + status: FlashingStatus, + moduleStatus: ModuleInstallStatus = ModuleInstallStatus(), + navigator: DestinationsNavigator, + onBack: () -> Unit = {}, + onSave: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + val cardColor = MaterialTheme.colorScheme.surfaceVariant + val cardAlpha = CardConfig.cardAlpha + + val statusColor = when(status) { + FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary + FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary + FlashingStatus.FAILED -> MaterialTheme.colorScheme.error + } + + TopAppBar( + title = { + Column { + Text( + text = stringResource( + when (status) { + FlashingStatus.FLASHING -> R.string.flashing + FlashingStatus.SUCCESS -> R.string.flash_success + FlashingStatus.FAILED -> R.string.flash_failed + } + ), + style = MaterialTheme.typography.titleLarge, + color = statusColor + ) + + if (moduleStatus.failedModules.isNotEmpty()) { + Text( + text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + } + }, + navigationIcon = { + IconButton(onClick = { + if (status != FlashingStatus.FLASHING) { + navigator.navigate(ModuleScreenDestination) { + } + } + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = cardColor.copy(alpha = cardAlpha), + scrolledContainerColor = cardColor.copy(alpha = cardAlpha) + ), + actions = { + IconButton(onClick = onSave) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = stringResource(id = R.string.save_log), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) +} + +suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String { + return withContext(Dispatchers.IO) { + try { + val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) + var entry = zipInputStream.nextEntry + var name = context.getString(R.string.unknown_module) + + while (entry != null) { + if (entry.name == "module.prop") { + val reader = java.io.BufferedReader(java.io.InputStreamReader(zipInputStream)) + var line: String? + while (reader.readLine().also { line = it } != null) { + if (line?.startsWith("name=") == true) { + name = line.substringAfter("=") + break + } + } + break + } + entry = zipInputStream.nextEntry + } + zipInputStream.close() + name + } catch (_: Exception) { + context.getString(R.string.unknown_module) } } } @@ -184,6 +529,7 @@ sealed class FlashIt : Parcelable { } fun flashIt( + context: android.content.Context, flashIt: FlashIt, onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, @@ -206,7 +552,7 @@ fun flashIt( } val currentUri = flashIt.uris[flashIt.currentIndex] - onStdout("Installing module ${flashIt.currentIndex + 1} of ${flashIt.uris.size}") + onStdout("\n") flashModule(currentUri, onFinish, onStdout, onStderr) } @@ -215,51 +561,6 @@ fun flashIt( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - status: FlashingStatus, - onBack: () -> Unit = {}, - onSave: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - TopAppBar( - title = { - Text( - stringResource( - when (status) { - FlashingStatus.FLASHING -> R.string.flashing - FlashingStatus.SUCCESS -> R.string.flash_success - FlashingStatus.FAILED -> R.string.flash_failed - } - ) - ) - }, - navigationIcon = { - IconButton( - onClick = onBack - ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - actions = { - IconButton(onClick = onSave) { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = "Localized description" - ) - } - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} - @Preview @Composable fun FlashScreenPreview() { 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 776a3eda..82bd0dee 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -357,4 +357,8 @@ 已选择 选择 禁用自定义卸载模块 + 错误代码 + 请查看日志 + 正在安装模块 %1$d/%2$d + %d 个模块安装失败 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 12e6c865..b3d5cb8f 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -361,4 +361,8 @@ Selected option Disable custom uninstallation module + error code + Please check the log + Module being installed %1$d/%2$d + %d Failed to install a new module