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 cae732e5..8b41efe9 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 @@ -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, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 4341d563..f06fa82a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -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, + 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 + ) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt index 22a9af2a..1a30a4cf 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt @@ -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() - 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 + ) { + // 显示确认对话框 + 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() - 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 + ) { + // 显示确认对话框 + 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 { + var showRestoreDialog by remember { mutableStateOf(false) } + var restoreConfirmResult by remember { mutableStateOf?>(null) } + var pendingUri by remember { mutableStateOf(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() + 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 { + var showAllowlistRestoreDialog by remember { mutableStateOf(false) } + var allowlistRestoreConfirmResult by remember { mutableStateOf?>(null) } + var pendingUri by remember { mutableStateOf(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() + allowlistRestoreConfirmResult = confirmResult + + restoreAllowlist( + context = context, + snackBarHost = snackBarHost, + uri = uri, + showConfirmDialog = { show -> showAllowlistRestoreDialog = show }, + confirmResult = confirmResult + ) + } } } }