manager: Add status tracking to ensure that brush-write operations are performed correctly

This commit is contained in:
ShirkNeko
2025-06-09 02:25:02 +08:00
parent 7b6f451cfb
commit c873ff74cb
3 changed files with 433 additions and 133 deletions

View File

@@ -110,6 +110,8 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
var tempText: String
val logContent = rememberSaveable { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
// 添加状态跟踪是否已经完成刷写
var hasFlashCompleted by rememberSaveable { mutableStateOf(false) }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
@@ -132,13 +134,19 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
totalModules = flashIt.uris.size,
currentModule = 1
)
hasFlashCompleted = false
} else if (flashIt !is FlashIt.FlashModules) {
hasFlashCompleted = false
}
}
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
// 只有在未完成刷写时才执行刷写操作
LaunchedEffect(flashIt, hasFlashCompleted) {
// 如果已经完成刷写或者已有文本内容,则不再执行
if (hasFlashCompleted || text.isNotEmpty()) {
return@LaunchedEffect
}
withContext(Dispatchers.IO) {
setFlashingStatus(FlashingStatus.FLASHING)
@@ -157,7 +165,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
}
}
flashIt(context, flashIt, onFinish = { showReboot, code ->
flashIt(flashIt, onFinish = { showReboot, code ->
if (code != 0) {
text += "$errorCodeString $code.\n$checkLogString\n"
setFlashingStatus(FlashingStatus.FAILED)
@@ -176,6 +184,8 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
showFloatAction = true
}
hasFlashCompleted = true
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
val nextFlashIt = flashIt.copy(
currentIndex = flashIt.currentIndex + 1
@@ -222,8 +232,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
TopBar(
currentFlashingStatus.value,
currentStatus,
navigator = navigator,
flashIt = flashIt,
onBack = onBack,
onSave = {
scope.launch {
@@ -434,8 +442,6 @@ fun ModuleInstallProgressBar(
private fun TopBar(
status: FlashingStatus,
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
navigator: DestinationsNavigator,
flashIt: FlashIt,
onBack: () -> Unit,
onSave: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
@@ -531,7 +537,6 @@ sealed class FlashIt : Parcelable {
}
fun flashIt(
context: android.content.Context,
flashIt: FlashIt,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,

View File

@@ -8,12 +8,20 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.*
import androidx.compose.material.icons.filled.*
@@ -24,6 +32,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
@@ -66,7 +76,6 @@ import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import java.util.concurrent.TimeUnit
import androidx.core.content.edit
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.webui.WebUIXActivity
import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri
@@ -74,6 +83,13 @@ import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.theme.getCardElevation
// 菜单项数据类
data class ModuleBottomSheetMenuItem(
val icon: ImageVector,
val titleRes: Int,
val onClick: () -> Unit
)
/**
* @author ShirkNeko
* @date 2025/5/31.
@@ -89,6 +105,12 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val confirmDialog = rememberConfirmDialog()
var lastClickTime by remember { mutableStateOf(0L) }
// BottomSheet状态
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
var showBottomSheet by remember { mutableStateOf(false) }
val selectZipLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
@@ -201,6 +223,34 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
contract = ActivityResultContracts.StartActivityForResult()
) { viewModel.fetchModuleList() }
// BottomSheet菜单项
val bottomSheetMenuItems = remember {
listOf(
ModuleBottomSheetMenuItem(
icon = Icons.Outlined.Save,
titleRes = R.string.backup_modules,
onClick = {
backupLauncher.launch(ModuleModify.createBackupIntent())
scope.launch {
bottomSheetState.hide()
showBottomSheet = false
}
}
),
ModuleBottomSheetMenuItem(
icon = Icons.Outlined.RestoreFromTrash,
titleRes = R.string.restore_modules,
onClick = {
restoreLauncher.launch(ModuleModify.createRestoreIntent())
scope.launch {
bottomSheetState.hide()
showBottomSheet = false
}
}
)
)
}
Scaffold(
topBar = {
SearchAppBar(
@@ -209,87 +259,13 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
dropdownContent = {
var showDropdown by remember { mutableStateOf(false) }
IconButton(
onClick = { showDropdown = true },
onClick = { showBottomSheet = true },
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings),
)
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_action_first)) },
trailingIcon = {
Checkbox(
checked = viewModel.sortActionFirst,
onCheckedChange = null,
)
},
onClick = {
viewModel.sortActionFirst = !viewModel.sortActionFirst
prefs.edit {
putBoolean(
"module_sort_action_first",
viewModel.sortActionFirst
)
}
scope.launch {
viewModel.fetchModuleList()
}
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
trailingIcon = {
Checkbox(
checked = viewModel.sortEnabledFirst,
onCheckedChange = null,
)
},
onClick = {
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
prefs.edit {
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
}
scope.launch {
viewModel.fetchModuleList()
}
}
)
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem(
text = { Text(stringResource(R.string.backup_modules)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.backup),
)
},
onClick = {
showDropdown = false
backupLauncher.launch(ModuleModify.createBackupIntent())
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.restore_modules)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.RestoreFromTrash,
contentDescription = stringResource(R.string.restore),
)
},
onClick = {
showDropdown = false
restoreLauncher.launch(ModuleModify.createRestoreIntent())
}
)
}
}
},
scrollBehavior = scrollBehavior,
@@ -425,6 +401,202 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
)
}
}
// BottomSheet
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = bottomSheetState,
dragHandle = {
Surface(
modifier = Modifier.padding(vertical = 11.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
shape = RoundedCornerShape(16.dp)
) {
Box(
Modifier.size(
width = 32.dp,
height = 4.dp
)
)
}
}
) {
ModuleBottomSheetContent(
menuItems = bottomSheetMenuItems,
viewModel = viewModel,
prefs = prefs,
scope = scope,
bottomSheetState = bottomSheetState,
onDismiss = { showBottomSheet = false }
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ModuleBottomSheetContent(
menuItems: List<ModuleBottomSheetMenuItem>,
viewModel: ModuleViewModel,
prefs: android.content.SharedPreferences,
scope: kotlinx.coroutines.CoroutineScope,
bottomSheetState: SheetState,
onDismiss: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
// 标题
Text(
text = stringResource(R.string.menu_options),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 菜单选项网格
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(menuItems) { menuItem ->
ModuleBottomSheetMenuItemView(
menuItem = menuItem
)
}
}
// 排序选项
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp))
Text(
text = stringResource(R.string.sort_options),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 排序选项
Column(
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 优先显示有操作的模块
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.module_sort_action_first),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = viewModel.sortActionFirst,
onCheckedChange = { checked ->
viewModel.sortActionFirst = checked
prefs.edit {
putBoolean("module_sort_action_first", checked)
}
scope.launch {
viewModel.fetchModuleList()
bottomSheetState.hide()
onDismiss()
}
}
)
}
// 优先显示已启用的模块
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.module_sort_enabled_first),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = viewModel.sortEnabledFirst,
onCheckedChange = { checked ->
viewModel.sortEnabledFirst = checked
prefs.edit {
putBoolean("module_sort_enabled_first", checked)
}
scope.launch {
viewModel.fetchModuleList()
bottomSheetState.hide()
onDismiss()
}
}
)
}
}
}
}
@Composable
private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) {
// 添加交互状态
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "menuItemScale"
)
Column(
modifier = Modifier
.fillMaxWidth()
.scale(scale)
.clickable(
interactionSource = interactionSource,
indication = null
) { menuItem.onClick() }
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface(
modifier = Modifier.size(48.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
imageVector = menuItem.icon,
contentDescription = stringResource(menuItem.titleRes),
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(menuItem.titleRes),
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
maxLines = 2
)
}
}

View File

@@ -1,17 +1,14 @@
package com.sukisu.ultra.ui.util
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -25,18 +22,78 @@ import java.util.Date
import java.util.Locale
object ModuleModify {
suspend fun showRestoreConfirmation(context: Context): Boolean {
val result = CompletableDeferred<Boolean>()
withContext(Dispatchers.Main) {
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.restore_confirm_title))
.setMessage(context.getString(R.string.restore_confirm_message))
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) }
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
.setOnCancelListener { result.complete(false) }
.show()
@Composable
fun RestoreConfirmationDialog(
showDialog: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = context.getString(R.string.restore_confirm_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = context.getString(R.string.restore_confirm_message),
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(context.getString(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(context.getString(R.string.cancel))
}
}
)
}
}
@Composable
fun AllowlistRestoreConfirmationDialog(
showDialog: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = context.getString(R.string.allowlist_restore_confirm_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = context.getString(R.string.allowlist_restore_confirm_message),
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(context.getString(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(context.getString(R.string.cancel))
}
}
)
}
return result.await()
}
suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
@@ -82,8 +139,19 @@ object ModuleModify {
}
}
suspend fun restoreModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
val userConfirmed = showRestoreConfirmation(context)
suspend fun restoreModules(
context: Context,
snackBarHost: SnackbarHostState,
uri: Uri,
showConfirmDialog: (Boolean) -> Unit,
confirmResult: CompletableDeferred<Boolean>
) {
// 显示确认对话框
withContext(Dispatchers.Main) {
showConfirmDialog(true)
}
val userConfirmed = confirmResult.await()
if (!userConfirmed) return
withContext(Dispatchers.IO) {
@@ -132,20 +200,6 @@ object ModuleModify {
}
}
suspend fun showAllowlistRestoreConfirmation(context: Context): Boolean {
val result = CompletableDeferred<Boolean>()
withContext(Dispatchers.Main) {
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.allowlist_restore_confirm_title))
.setMessage(context.getString(R.string.allowlist_restore_confirm_message))
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) }
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
.setOnCancelListener { result.complete(false) }
.show()
}
return result.await()
}
suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
withContext(Dispatchers.IO) {
try {
@@ -182,8 +236,19 @@ object ModuleModify {
}
}
suspend fun restoreAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
val userConfirmed = showAllowlistRestoreConfirmation(context)
suspend fun restoreAllowlist(
context: Context,
snackBarHost: SnackbarHostState,
uri: Uri,
showConfirmDialog: (Boolean) -> Unit,
confirmResult: CompletableDeferred<Boolean>
) {
// 显示确认对话框
withContext(Dispatchers.Main) {
showConfirmDialog(true)
}
val userConfirmed = confirmResult.await()
if (!userConfirmed) return
withContext(Dispatchers.IO) {
@@ -246,13 +311,42 @@ object ModuleModify {
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
restoreModules(context, snackBarHost, uri)
): androidx.activity.result.ActivityResultLauncher<Intent> {
var showRestoreDialog by remember { mutableStateOf(false) }
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
var pendingUri by remember { mutableStateOf<Uri?>(null) }
// 显示恢复确认对话框
RestoreConfirmationDialog(
showDialog = showRestoreDialog,
onConfirm = {
showRestoreDialog = false
restoreConfirmResult?.complete(true)
},
onDismiss = {
showRestoreDialog = false
restoreConfirmResult?.complete(false)
}
)
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
pendingUri = uri
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
restoreConfirmResult = confirmResult
restoreModules(
context = context,
snackBarHost = snackBarHost,
uri = uri,
showConfirmDialog = { show -> showRestoreDialog = show },
confirmResult = confirmResult
)
}
}
}
}
@@ -280,13 +374,42 @@ object ModuleModify {
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
restoreAllowlist(context, snackBarHost, uri)
): androidx.activity.result.ActivityResultLauncher<Intent> {
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
var pendingUri by remember { mutableStateOf<Uri?>(null) }
// 显示允许列表恢复确认对话框
AllowlistRestoreConfirmationDialog(
showDialog = showAllowlistRestoreDialog,
onConfirm = {
showAllowlistRestoreDialog = false
allowlistRestoreConfirmResult?.complete(true)
},
onDismiss = {
showAllowlistRestoreDialog = false
allowlistRestoreConfirmResult?.complete(false)
}
)
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
pendingUri = uri
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
allowlistRestoreConfirmResult = confirmResult
restoreAllowlist(
context = context,
snackBarHost = snackBarHost,
uri = uri,
showConfirmDialog = { show -> showAllowlistRestoreDialog = show },
confirmResult = confirmResult
)
}
}
}
}