From 70f7ffb5fc3f201ec2b453cb60e541f714b71480 Mon Sep 17 00:00:00 2001 From: Andre K Date: Mon, 8 May 2023 17:31:07 -0300 Subject: [PATCH] feat: implement `PacketResponseState.Loading` (#630) --- .../mesh/ui/DeviceSettingsFragment.kt | 93 ++++++++++++------- .../config/EditDeviceProfileDialog.kt | 10 +- .../config/PacketResponseStateDialog.kt | 69 ++++++++++++++ 3 files changed, 133 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt diff --git a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index b8340f0f..bb0761fe 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -79,6 +79,7 @@ import com.geeksville.mesh.ui.components.config.ExternalNotificationConfigItemLi import com.geeksville.mesh.ui.components.config.LoRaConfigItemList import com.geeksville.mesh.ui.components.config.MQTTConfigItemList import com.geeksville.mesh.ui.components.config.NetworkConfigItemList +import com.geeksville.mesh.ui.components.config.PacketResponseStateDialog import com.geeksville.mesh.ui.components.config.PositionConfigItemList import com.geeksville.mesh.ui.components.config.PowerConfigItemList import com.geeksville.mesh.ui.components.config.RangeTestConfigItemList @@ -134,6 +135,20 @@ enum class ModuleDest(val title: String, val route: String, val config: ModuleCo REMOTE_HARDWARE("Remote Hardware", "remote_hardware", ModuleConfigType.REMOTEHARDWARE_CONFIG); } +/** + * This sealed class defines each possible state of a packet response. + */ +sealed class PacketResponseState { + object Loading : PacketResponseState() { + var total: Int = 0 + var completed: Int = 0 + } + + data class Success(val packets: List) : PacketResponseState() + object Empty : PacketResponseState() + data class Error(val error: String) : PacketResponseState() +} + @Composable fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { val navController = rememberNavController() @@ -156,12 +171,15 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { val configResponse by viewModel.packetResponse.collectAsStateWithLifecycle() val deviceProfile by viewModel.deviceProfile.collectAsStateWithLifecycle() - var isWaiting by remember { mutableStateOf(false) } + var packetResponseState by remember { mutableStateOf(PacketResponseState.Empty) } + val isWaiting = packetResponseState is PacketResponseState.Loading + var showEditDeviceProfileDialog by remember { mutableStateOf(false) } val importConfigLauncher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { + showEditDeviceProfileDialog = true it.data?.data?.let { file_uri -> viewModel.importProfile(file_uri) } } } @@ -174,10 +192,9 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { } } - var showEditDeviceProfileDialog by remember { mutableStateOf(false) } if (showEditDeviceProfileDialog) EditDeviceProfileDialog( - title = "Export configuration", - deviceProfile = with(viewModel) { + title = if (deviceProfile != null) "Import configuration" else "Export configuration", + deviceProfile = deviceProfile ?: with(viewModel) { deviceProfile { ourNodeInfo.value?.user?.let { longName = it.longName @@ -189,37 +206,32 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { } }, onAddClick = { - isWaiting = false - viewModel.setDeviceProfile(it) - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - putExtra(Intent.EXTRA_TITLE, "${destNum.toUInt()}.cfg") - } - exportConfigLauncher.launch(intent) showEditDeviceProfileDialog = false + if (deviceProfile != null) { + viewModel.installProfile(it) + } else { + viewModel.setDeviceProfile(it) + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/*" + putExtra(Intent.EXTRA_TITLE, "${destNum.toUInt()}.cfg") + } + exportConfigLauncher.launch(intent) + } }, onDismissRequest = { - isWaiting = false showEditDeviceProfileDialog = false viewModel.setDeviceProfile(null) } ) - if (isWaiting && deviceProfile != null) { - EditDeviceProfileDialog( - title = "Import configuration", - deviceProfile = deviceProfile ?: return, - onAddClick = { - isWaiting = false - viewModel.installProfile(it) - }, - onDismissRequest = { - isWaiting = false - viewModel.setDeviceProfile(null) - } - ) - } + if (isWaiting) PacketResponseStateDialog( + packetResponseState as PacketResponseState.Loading, + onDismiss = { + packetResponseState = PacketResponseState.Empty + viewModel.clearPacketResponse() + } + ) if (isWaiting) LaunchedEffect(configResponse) { val data = configResponse?.meshPacket?.decoded @@ -233,28 +245,29 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { // Stop once we get to the first disabled entry if (response.role != ChannelProtos.Channel.Role.DISABLED) { // Not done yet, request next channel + (packetResponseState as PacketResponseState.Loading).completed++ channelList.add(response.index, response.settings) viewModel.getChannel(destNum, response.index + 1) } else { // Received the last channel, start channel editor - isWaiting = false + packetResponseState = PacketResponseState.Success(emptyList()) navController.navigate("channels") } } else { // Received max channels, start channel editor - isWaiting = false + packetResponseState = PacketResponseState.Success(emptyList()) navController.navigate("channels") } } AdminProtos.AdminMessage.PayloadVariantCase.GET_OWNER_RESPONSE -> { - isWaiting = false + packetResponseState = PacketResponseState.Success(emptyList()) userConfig = parsed.getOwnerResponse navController.navigate("user") } AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> { - isWaiting = false + packetResponseState = PacketResponseState.Success(emptyList()) val response = parsed.getConfigResponse radioConfig = response enumValues().find { it.name == "${response.payloadVariantCase}" } @@ -262,7 +275,7 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { } AdminProtos.AdminMessage.PayloadVariantCase.GET_MODULE_CONFIG_RESPONSE -> { - isWaiting = false + packetResponseState = PacketResponseState.Success(emptyList()) val response = parsed.getModuleConfigResponse moduleConfig = response enumValues().find { it.name == "${response.payloadVariantCase}" } @@ -271,11 +284,13 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { AdminProtos.AdminMessage.PayloadVariantCase.GET_CANNED_MESSAGE_MODULE_MESSAGES_RESPONSE -> { cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse + (packetResponseState as PacketResponseState.Loading).completed++ viewModel.getModuleConfig(destNum, ModuleConfigType.CANNEDMSG_CONFIG_VALUE) } AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> { ringtone = parsed.getRingtoneResponse + (packetResponseState as PacketResponseState.Loading).completed++ viewModel.getModuleConfig(destNum, ModuleConfigType.EXTNOTIF_CONFIG_VALUE) } @@ -291,15 +306,20 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { isLocal = destNum == viewModel.myNodeNum, headerText = node.user?.longName ?: stringResource(R.string.unknown_username), onRouteClick = { configType -> - isWaiting = true + packetResponseState = PacketResponseState.Loading.apply { + total = 1 + completed = 0 + } // clearAllConfigs() ? when (configType) { "USER" -> { viewModel.getOwner(destNum) } "CHANNELS" -> { + (packetResponseState as PacketResponseState.Loading).total = maxChannels channelList.clear() viewModel.getChannel(destNum, 0) } "IMPORT" -> { + packetResponseState = PacketResponseState.Empty viewModel.setDeviceProfile(null) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) @@ -307,14 +327,19 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { } importConfigLauncher.launch(intent) } - "EXPORT" -> { showEditDeviceProfileDialog = true } + "EXPORT" -> { + packetResponseState = PacketResponseState.Empty + showEditDeviceProfileDialog = true + } is ConfigType -> { viewModel.getConfig(destNum, configType.number) } ModuleConfigType.CANNEDMSG_CONFIG -> { + (packetResponseState as PacketResponseState.Loading).total = 2 viewModel.getCannedMessages(destNum) } ModuleConfigType.EXTNOTIF_CONFIG -> { + (packetResponseState as PacketResponseState.Loading).total = 2 viewModel.getRingtone(destNum) } is ModuleConfigType -> { diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt index edf15c3e..67d1b1f8 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt @@ -31,11 +31,11 @@ fun EditDeviceProfileDialog( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { - var longNameInput by remember { mutableStateOf(deviceProfile.hasLongName()) } - var shortNameInput by remember { mutableStateOf(deviceProfile.hasShortName()) } - var channelUrlInput by remember { mutableStateOf(deviceProfile.hasChannelUrl()) } - var configInput by remember { mutableStateOf(deviceProfile.hasConfig()) } - var moduleConfigInput by remember { mutableStateOf(deviceProfile.hasModuleConfig()) } + var longNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasLongName()) } + var shortNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasShortName()) } + var channelUrlInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasChannelUrl()) } + var configInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasConfig()) } + var moduleConfigInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasModuleConfig()) } AlertDialog( title = { Text(title) }, diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt new file mode 100644 index 00000000..fe90b45f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt @@ -0,0 +1,69 @@ +package com.geeksville.mesh.ui.components.config + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.geeksville.mesh.R +import com.geeksville.mesh.ui.PacketResponseState + +@Composable +fun PacketResponseStateDialog( + state: PacketResponseState.Loading, + onDismiss: () -> Unit +) { + val progress = state.completed.toFloat() / state.total.toFloat() + AlertDialog( + onDismissRequest = { }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("%.0f%%".format(progress * 100)) + LinearProgressIndicator( + progress = progress, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + color = MaterialTheme.colors.onSurface, + ) + } + }, + buttons = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button( + onClick = onDismiss, + modifier = Modifier.padding(bottom = 16.dp) + ) { Text(stringResource(R.string.cancel)) } + } + } + ) +} + +@Preview(showBackground = true) +@Composable +fun PacketResponseStateDialogPreview() { + PacketResponseStateDialog( + state = PacketResponseState.Loading.apply { + total = 17 + completed = 5 + }, + onDismiss = { } + ) +}