From ddb19b959fc51fa83a0843f3489fa8bda6ebb59a Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:59:33 -0500 Subject: [PATCH] refactor(settings)!: standardize radio config screens (#3167) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../baseline.xml => app/detekt-baseline.xml | 136 ++----- .../mesh/navigation/SettingsNavigation.kt | 55 +-- .../main/java/com/geeksville/mesh/ui/Main.kt | 27 +- .../mesh/ui/settings/SettingsScreen.kt | 9 +- .../AmbientLightingConfigItemList.kt | 99 ++--- .../radio/components/AudioConfigItemList.kt | 106 ++--- .../components/BluetoothConfigItemList.kt | 82 ++-- .../components/CannedMessageConfigItemList.kt | 144 +++---- .../components/ChannelSettingsItemList.kt | 196 +++++---- .../settings/radio/components/ConfigState.kt | 70 ++++ .../DetectionSensorConfigItemList.kt | 117 ++---- .../radio/components/DeviceConfigItemList.kt | 275 ++++++------- .../radio/components/DisplayConfigItemList.kt | 127 +++--- .../ExternalNotificationConfigItemList.kt | 194 ++++----- .../radio/components/LoRaConfigItemList.kt | 178 ++++----- .../radio/components/MQTTConfigItemList.kt | 186 ++++----- .../components/NeighborInfoConfigItemList.kt | 87 ++-- .../radio/components/NetworkConfigItemList.kt | 184 ++++----- .../components/PaxcounterConfigItemList.kt | 94 ++--- .../components/PositionConfigItemList.kt | 203 ++++------ .../radio/components/PowerConfigItemList.kt | 124 ++---- .../radio/components/RadioConfigScreenList.kt | 81 ++++ .../components/RangeTestConfigItemList.kt | 83 ++-- .../RemoteHardwareConfigItemList.kt | 89 ++--- .../components/SecurityConfigItemList.kt | 177 ++++---- .../radio/components/SerialConfigItemList.kt | 109 ++--- .../components/StoreForwardConfigItemList.kt | 105 ++--- .../components/TelemetryConfigItemList.kt | 125 ++---- .../radio/components/UserConfigItemList.kt | 120 ++---- .../com/geeksville/mesh/buildlogic/Detekt.kt | 1 - .../detekt-baseline-meshserviceexample.xml | 102 ----- config/detekt/detekt-baseline.xml | 378 ------------------ core/model/detekt-baseline.xml | 30 ++ core/strings/src/main/res/values/strings.xml | 1 + mesh_service_example/detekt-baseline.xml | 37 ++ 35 files changed, 1480 insertions(+), 2651 deletions(-) rename config/detekt/baseline.xml => app/detekt-baseline.xml (78%) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ConfigState.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt delete mode 100644 config/detekt/detekt-baseline-meshserviceexample.xml delete mode 100644 config/detekt/detekt-baseline.xml create mode 100644 core/model/detekt-baseline.xml create mode 100644 mesh_service_example/detekt-baseline.xml diff --git a/config/detekt/baseline.xml b/app/detekt-baseline.xml similarity index 78% rename from config/detekt/baseline.xml rename to app/detekt-baseline.xml index 66c9e45ca..31605b0d9 100644 --- a/config/detekt/baseline.xml +++ b/app/detekt-baseline.xml @@ -2,11 +2,8 @@ - ChainWrapping:Channel.kt$Channel$&& CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back - CommentSpacing:ContextExtensions.kt$/// Utility function to hide the soft keyboard per stack overflow - CommentSpacing:ContextExtensions.kt$/// show a toast CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work @@ -18,8 +15,6 @@ ComposableParamOrder:AlertDialogs.kt$SimpleAlertDialog ComposableParamOrder:BatteryInfo.kt$BatteryInfo ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList - ComposableParamOrder:Connections.kt$ConnectionsScreen - ComposableParamOrder:CurrentlyConnectedCard.kt$CurrentlyConnectedCard ComposableParamOrder:Debug.kt$DebugMenuActions ComposableParamOrder:Debug.kt$DecodedPayloadBlock ComposableParamOrder:DebugSearch.kt$DebugSearchState @@ -51,7 +46,6 @@ ComposableParamOrder:NodeDetail.kt$EnvironmentMetrics ComposableParamOrder:NodeDetail.kt$NodeActionButton ComposableParamOrder:NodeDetail.kt$NodeDetailList - ComposableParamOrder:NodeDetail.kt$NodeDetailScreen ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField ComposableParamOrder:NodeItem.kt$NodeItem ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon @@ -59,11 +53,9 @@ ComposableParamOrder:NodeScreen.kt$NodeScreen ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout - ComposableParamOrder:PositionConfigItemList.kt$PositionConfigItemList ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo - ComposableParamOrder:SecurityConfigItemList.kt$SecurityConfigItemList ComposableParamOrder:SettingsItem.kt$SettingsItem ComposableParamOrder:SignalInfo.kt$SignalInfo ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart @@ -80,6 +72,8 @@ ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long ContentSlotReused:AdaptiveTwoPane.kt$second CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) + CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) EmptyCatchBlock:MeshLog.kt$MeshLog${ } EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ } @@ -117,7 +111,6 @@ FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long) ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel) LambdaParameterEventTrailing:Channel.kt$onConfirm - LambdaParameterEventTrailing:CurrentlyConnectedCard.kt$onClickDisconnect LambdaParameterEventTrailing:MainAppBar.kt$onAction LambdaParameterEventTrailing:Message.kt$onClick LambdaParameterEventTrailing:Message.kt$onSendMessage @@ -128,15 +121,24 @@ LambdaParameterInRestartableEffect:Channel.kt$onConfirm LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged LargeClass:MeshService.kt$MeshService : ServiceLogging - LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigItemList( ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, enabled: Boolean, onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit, ) - LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigItemList( messages: String, cannedMessageConfig: CannedMessageConfig, enabled: Boolean, onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit, ) - LongMethod:DropDownPreference.kt$@Composable fun <T> DropDownPreference( title: String, enabled: Boolean, items: List<Pair<T, String>>, selectedItem: T, onItemSelected: (T) -> Unit, modifier: Modifier = Modifier, summary: String? = null, ) - LongMethod:EditListPreference.kt$@Composable inline fun <reified T> EditListPreference( title: String, list: List<T>, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List<T>) -> Unit, modifier: Modifier = Modifier, ) - LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, ) + LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) + LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) - LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigItemList( storeForwardConfig: StoreForwardConfig, enabled: Boolean, onSaveClicked: (StoreForwardConfig) -> Unit, ) - LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigItemList( telemetryConfig: TelemetryConfig, enabled: Boolean, onSaveClicked: (TelemetryConfig) -> Unit, ) + LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) + LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) MagicNumber:BatteryInfo.kt$100 MagicNumber:BatteryInfo.kt$101 MagicNumber:BatteryInfo.kt$14 @@ -150,30 +152,6 @@ MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000 MagicNumber:BluetoothInterface.kt$BluetoothInterface$500 MagicNumber:BluetoothInterface.kt$BluetoothInterface$512 - MagicNumber:Channel.kt$0xff - MagicNumber:ChannelOption.kt$.03125f - MagicNumber:ChannelOption.kt$.0625f - MagicNumber:ChannelOption.kt$.203125f - MagicNumber:ChannelOption.kt$.40625f - MagicNumber:ChannelOption.kt$.8125f - MagicNumber:ChannelOption.kt$1.6250f - MagicNumber:ChannelOption.kt$1000f - MagicNumber:ChannelOption.kt$1600 - MagicNumber:ChannelOption.kt$200 - MagicNumber:ChannelOption.kt$3.25f - MagicNumber:ChannelOption.kt$31 - MagicNumber:ChannelOption.kt$400 - MagicNumber:ChannelOption.kt$5 - MagicNumber:ChannelOption.kt$62 - MagicNumber:ChannelOption.kt$800 - MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f - MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f - MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f - MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f - MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f MagicNumber:ChannelSet.kt$40 MagicNumber:ChannelSet.kt$960 MagicNumber:Contacts.kt$7 @@ -239,6 +217,7 @@ MagicNumber:NodeInfo.kt$Position.Companion$1e7 MagicNumber:PacketRepository.kt$PacketRepository$500 MagicNumber:PacketResponseStateDialog.kt$100 + MagicNumber:PowerConfigItemList.kt$3600 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809 MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790 @@ -266,7 +245,6 @@ MatchingDeclarationName:SortOption.kt$NodeSortOption MaxLineLength:BluetoothInterface.kt$/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface. MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there. FIXME - make sure this protocol is guaranteed robust and won't drop packets "According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)). In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes." MAXPACKET is 256? look into what the lora lib uses. FIXME Characteristics: UUID properties description 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 read fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet). After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this mailbox. f75c76d2-129e-4dad-a1dd-7866124401e7 write toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len) ed9da18c-a800-4f66-a670-aa7547e34453 read|notify|write fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages until it catches up with this number. The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32 callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio. When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio. Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted. Re: queue management Not all messages are kept in the fromradio queue (filtered based on SubPacket): * only the most recent Position and User messages for a particular node are kept * all Data SubPackets are kept * No WantNodeNum / DenyNodeNum messages are kept A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging) */ MaxLineLength:BluetoothState.kt$BluetoothState$"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})" - MaxLineLength:Channel.kt$Channel$// We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m") MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$* @@ -275,65 +253,49 @@ ModifierClickableOrder:Channel.kt$clickable(onClick = onClick) ModifierListSpacing:Packet.kt$Packet$@Entity( tableName = "packet", indices = [ Index(value = ["myNodeNum"]), Index(value = ["port_num"]), Index(value = ["contact_key"]), ] ) data ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane - ModifierMissing:AmbientLightingConfigItemList.kt$AmbientLightingConfigItemList - ModifierMissing:AudioConfigItemList.kt$AudioConfigItemList ModifierMissing:BLEDevices.kt$BLEDevices - ModifierMissing:BluetoothConfigItemList.kt$BluetoothConfigItemList - ModifierMissing:CannedMessageConfigItemList.kt$CannedMessageConfigItemList ModifierMissing:Channel.kt$ChannelScreen ModifierMissing:ChannelSettingsItemList.kt$ChannelSelection ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen ModifierMissing:CommonCharts.kt$ChartHeader ModifierMissing:CommonCharts.kt$Legend ModifierMissing:CommonCharts.kt$TimeLabels - ModifierMissing:Connections.kt$ConnectionsScreen ModifierMissing:ContactSharing.kt$SharedContactDialog ModifierMissing:Contacts.kt$ContactListView ModifierMissing:Contacts.kt$ContactsScreen ModifierMissing:Contacts.kt$SelectionToolbar - ModifierMissing:DetectionSensorConfigItemList.kt$DetectionSensorConfigItemList - ModifierMissing:DeviceConfigItemList.kt$DeviceConfigItemList ModifierMissing:DeviceMetrics.kt$DeviceMetricsScreen - ModifierMissing:DisplayConfigItemList.kt$DisplayConfigItemList ModifierMissing:EmojiPicker.kt$EmojiPicker ModifierMissing:EmojiPicker.kt$EmojiPickerDialog ModifierMissing:EmptyStateContent.kt$EmptyStateContent ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen - ModifierMissing:ExternalNotificationConfigItemList.kt$ExternalNotificationConfigItemList ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality - ModifierMissing:LoRaConfigItemList.kt$LoRaConfigItemList ModifierMissing:LoraSignalIndicator.kt$LoraSignalIndicator ModifierMissing:LoraSignalIndicator.kt$Rssi ModifierMissing:LoraSignalIndicator.kt$Snr ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi - ModifierMissing:MQTTConfigItemList.kt$MQTTConfigItemList ModifierMissing:Main.kt$MainScreen ModifierMissing:MapReportingPreference.kt$MapReportingPreference ModifierMissing:MessageActions.kt$MessageStatusButton ModifierMissing:MessageActions.kt$ReactionButton ModifierMissing:MessageActions.kt$ReplyButton - ModifierMissing:NeighborInfoConfigItemList.kt$NeighborInfoConfigItemList - ModifierMissing:NetworkConfigItemList.kt$NetworkConfigItemList + ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen ModifierMissing:NetworkDevices.kt$NetworkDevices ModifierMissing:NodeMenu.kt$NodeMenu ModifierMissing:NodeScreen.kt$NodeScreen ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons ModifierMissing:PaxMetrics.kt$PaxMetricsItem ModifierMissing:PaxMetrics.kt$PaxMetricsScreen - ModifierMissing:PaxcounterConfigItemList.kt$PaxcounterConfigItemList - ModifierMissing:PositionConfigItemList.kt$PositionConfigItemList + ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen ModifierMissing:PositionLog.kt$PositionItem ModifierMissing:PositionLog.kt$PositionLogScreen - ModifierMissing:PowerConfigItemList.kt$PowerConfigItemList ModifierMissing:PowerMetrics.kt$PowerMetricsScreen ModifierMissing:RadioConfig.kt$RadioConfigItemList - ModifierMissing:RangeTestConfigItemList.kt$RangeTestConfigItemList + ModifierMissing:RadioConfigScreenList.kt$RadioConfigScreenList ModifierMissing:Reaction.kt$ReactionDialog - ModifierMissing:RemoteHardwareConfigItemList.kt$RemoteHardwareConfigItemList - ModifierMissing:SecurityConfigItemList.kt$SecurityConfigItemList + ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen ModifierMissing:SecurityIcon.kt$SecurityIcon - ModifierMissing:SerialConfigItemList.kt$SerialConfigItemList ModifierMissing:SettingsItem.kt$SettingsItem ModifierMissing:SettingsItem.kt$SettingsItemDetail ModifierMissing:SettingsItem.kt$SettingsItemSwitch @@ -342,20 +304,12 @@ ModifierMissing:SignalMetrics.kt$SignalMetricsScreen ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog ModifierMissing:SlidingSelector.kt$OptionLabel - ModifierMissing:StoreForwardConfigItemList.kt$StoreForwardConfigItemList - ModifierMissing:TelemetryConfigItemList.kt$TelemetryConfigItemList ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon - ModifierMissing:UserConfigItemList.kt$UserConfigItemList - ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End) - ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier.fillMaxWidth() ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT) ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp) ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier.width(dp) - ModifierNotUsedAtRoot:DropDownPreference.kt$modifier = modifier .background( color = if (selectedItem == item.first) { MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) } else { Color.Unspecified }, ) ModifierNotUsedAtRoot:EditChannelDialog.kt$modifier = modifier.weight(1f) ModifierNotUsedAtRoot:EditDeviceProfileDialog.kt$modifier = modifier.weight(1f) - ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.fillMaxWidth() - ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.padding(16.dp) ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier = modifier.width(dp) ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier.width(dp) ModifierNotUsedAtRoot:NodeChip.kt$modifier = modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } } @@ -370,17 +324,9 @@ ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp) ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .padding(all = 16.dp) ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End) - ModifierReused:BitwisePreference.kt$Checkbox( modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, ) - ModifierReused:BitwisePreference.kt$DropdownMenuItem( onClick = { onItemSelected(value xor item.first) }, modifier = modifier.fillMaxWidth(), text = { Text( text = item.second, overflow = TextOverflow.Ellipsis, ) Checkbox( modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, ) } ) ModifierReused:DeviceMetrics.kt$Canvas(modifier = modifier.width(dp)) { val height = size.height val width = size.width for (i in telemetries.indices) { val telemetry = telemetries[i] /* x-value time */ val xRatio = (telemetry.time - oldest.time).toFloat() / timeDiff val x = xRatio * width /* Channel Utilization */ plotPoint( drawContext = drawContext, color = Device.CH_UTIL.color, x = x, value = telemetry.deviceMetrics.channelUtilization, divisor = MAX_PERCENT_VALUE, ) /* Air Utilization Transmit */ plotPoint( drawContext = drawContext, color = Device.AIR_UTIL.color, x = x, value = telemetry.deviceMetrics.airUtilTx, divisor = MAX_PERCENT_VALUE, ) } /* Battery Line */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } } ModifierReused:DeviceMetrics.kt$HorizontalLinesOverlay( modifier.width(dp), lineColors = listOf(graphColor, Color.Yellow, Color.Red, graphColor, graphColor), ) ModifierReused:DeviceMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval()) - ModifierReused:EditListPreference.kt$Column(modifier = modifier) { Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium) listState.forEachIndexed { index, value -> val trailingIcon = @Composable { IconButton( onClick = { focusManager.clearFocus() listState.removeAt(index) onValuesChanged(listState) }, ) { Icon( imageVector = Icons.TwoTone.Close, contentDescription = stringResource(R.string.delete), modifier = Modifier.wrapContentSize(), ) } } // handle lora.ignoreIncoming: List<Int> if (value is Int) { EditTextPreference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) } // handle security.adminKey: List<ByteString> if (value is ByteString) { EditBase64Preference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChange = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) } // handle remoteHardware.availablePins: List<RemoteHardwarePin> if (value is RemoteHardwarePin) { EditTextPreference( title = stringResource(R.string.gpio_pin), value = value.gpioPin, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { if (it in 0..255) { listState[index] = value.copy { gpioPin = it } as T onValuesChanged(listState) } }, ) EditTextPreference( title = stringResource(R.string.name), value = value.name, maxSize = 14, // name max_size:15 enabled = enabled, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { listState[index] = value.copy { name = it } as T onValuesChanged(listState) }, trailingIcon = trailingIcon, ) DropDownPreference( title = stringResource(R.string.type), enabled = enabled, items = RemoteHardwarePinType.entries .filter { it != RemoteHardwarePinType.UNRECOGNIZED } .map { it to it.name }, selectedItem = value.type, onItemSelected = { listState[index] = value.copy { type = it } as T onValuesChanged(listState) }, ) } } OutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = { // Add element based on the type T val newElement = when (T::class) { Int::class -> 0 as T ByteString::class -> ByteString.EMPTY as T RemoteHardwarePin::class -> remoteHardwarePin {} as T else -> throw IllegalArgumentException("Unsupported type: ${T::class}") } listState.add(listState.size, newElement) }, enabled = maxCount > listState.size, ) { Text(text = stringResource(R.string.add)) } } - ModifierReused:EditListPreference.kt$EditBase64Preference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChange = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) - ModifierReused:EditListPreference.kt$EditTextPreference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) - ModifierReused:EditListPreference.kt$Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium) - ModifierReused:EditTextPreference.kt$Box( contentAlignment = Alignment.BottomEnd, modifier = modifier.fillMaxWidth() ) { Text( text = "${value.toByteArray().size}/$maxSize", style = MaterialTheme.typography.bodySmall, color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(end = 8.dp, bottom = 4.dp) ) } - ModifierReused:EditTextPreference.kt$TextField( value = value, singleLine = true, modifier = modifier .fillMaxWidth() .onFocusEvent { isFocused = it.isFocused; onFocusChanged(it) }, enabled = enabled, isError = isError, onValueChange = { if (maxSize > 0) { if (it.toByteArray().size <= maxSize) { onValueChanged(it) } } else onValueChanged(it) }, label = { Text(title) }, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, visualTransformation = visualTransformation, trailingIcon = { if (trailingIcon != null) { trailingIcon() } else if (isError) { Icon( imageVector = Icons.TwoTone.Info, contentDescription = stringResource(id = R.string.error), tint = MaterialTheme.colorScheme.error ) } }, ) ModifierReused:EnvironmentCharts.kt$Box( contentAlignment = Alignment.TopStart, modifier = modifier.horizontalScroll(state = scrollState, reverseScrolling = true), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval()) MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, ) } ModifierReused:EnvironmentCharts.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor }) ModifierReused:EnvironmentCharts.kt$MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, ) @@ -408,18 +354,10 @@ ModifierReused:TextDividerPreference.kt$Row( modifier = modifier .fillMaxWidth() .padding(all = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = title, style = MaterialTheme.typography.bodyLarge, color = if (!enabled) { MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } else { Color.Unspecified }, ) if (trailingIcon != null) { Icon( trailingIcon, "trailingIcon", modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), ) } } ModifierWithoutDefault:CommonCharts.kt$modifier ModifierWithoutDefault:EnvironmentCharts.kt$modifier - MultiLineIfElse:Channel.kt$Channel$"Custom" - MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" } - MultiLineIfElse:EditTextPreference.kt$it.toDoubleOrNull()?.let { double -> valueState = it onValueChanged(double) } - MultiLineIfElse:EditTextPreference.kt$it.toFloatOrNull()?.let { float -> valueState = it onValueChanged(float) } - MultiLineIfElse:EditTextPreference.kt$it.toUIntOrNull()?.toInt()?.let { int -> valueState = it onValueChanged(int) } - MultiLineIfElse:EditTextPreference.kt$onValueChanged(it) - MultiLineIfElse:EditTextPreference.kt$valueState = it MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex) MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview MultipleEmitters:CommonCharts.kt$LegendLabel MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart - MultipleEmitters:EditTextPreference.kt$EditTextPreference MultipleEmitters:EnvironmentCharts.kt$EnvironmentMetricsChart MultipleEmitters:NodeDetail.kt$EncryptionErrorContent MultipleEmitters:NodeDetail.kt$MetricsSection @@ -430,7 +368,6 @@ MultipleEmitters:SignalMetrics.kt$SignalMetricsChart MutableStateAutoboxing:Contacts.kt$mutableStateOf(2) MutableStateParam:MessageList.kt$selectedIds - NestedBlockDepth:LanguageUtils.kt$LanguageUtils$fun getLanguageTags(context: Context): Map<String, String> NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) @@ -472,54 +409,32 @@ NoSemicolons:DateUtils.kt$DateUtils$; NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.* OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract - ParameterNaming:AmbientLightingConfigItemList.kt$onSaveClicked - ParameterNaming:AudioConfigItemList.kt$onSaveClicked ParameterNaming:BitwisePreference.kt$onItemSelected - ParameterNaming:BluetoothConfigItemList.kt$onSaveClicked - ParameterNaming:CannedMessageConfigItemList.kt$onSaveClicked ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked ParameterNaming:ChannelSettingsItemList.kt$onSelected ParameterNaming:CleanNodeDatabaseScreen.kt$onCheckedChanged ParameterNaming:CleanNodeDatabaseScreen.kt$onDaysChanged ParameterNaming:Contacts.kt$onDeleteSelected ParameterNaming:Contacts.kt$onMuteSelected - ParameterNaming:DetectionSensorConfigItemList.kt$onSaveClicked - ParameterNaming:DeviceConfigItemList.kt$onSaveClicked - ParameterNaming:DisplayConfigItemList.kt$onSaveClicked ParameterNaming:DropDownPreference.kt$onItemSelected ParameterNaming:EditIPv4Preference.kt$onValueChanged ParameterNaming:EditListPreference.kt$onValuesChanged ParameterNaming:EditPasswordPreference.kt$onValueChanged ParameterNaming:EditTextPreference.kt$onValueChanged - ParameterNaming:ExternalNotificationConfigItemList.kt$onSaveClicked - ParameterNaming:LoRaConfigItemList.kt$onSaveClicked - ParameterNaming:MQTTConfigItemList.kt$onSaveClicked - ParameterNaming:MQTTConfigItemList.kt$onShouldReportLocationChanged ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged ParameterNaming:MapReportingPreference.kt$onPositionPrecisionChanged ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged ParameterNaming:MessageList.kt$onUnreadChanged - ParameterNaming:NeighborInfoConfigItemList.kt$onSaveClicked - ParameterNaming:NetworkConfigItemList.kt$onSaveClicked ParameterNaming:NodeDetail.kt$onFirmwareSelected ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored - ParameterNaming:PaxcounterConfigItemList.kt$onSaveClicked - ParameterNaming:PositionConfigItemList.kt$onSaveClicked ParameterNaming:PositionPrecisionPreference.kt$onValueChanged - ParameterNaming:PowerConfigItemList.kt$onSaveClicked ParameterNaming:PreferenceFooter.kt$onCancelClicked ParameterNaming:PreferenceFooter.kt$onNegativeClicked ParameterNaming:PreferenceFooter.kt$onPositiveClicked ParameterNaming:PreferenceFooter.kt$onSaveClicked - ParameterNaming:RangeTestConfigItemList.kt$onSaveClicked - ParameterNaming:RemoteHardwareConfigItemList.kt$onSaveClicked - ParameterNaming:SerialConfigItemList.kt$onSaveClicked ParameterNaming:SlidingSelector.kt$onOptionSelected - ParameterNaming:StoreForwardConfigItemList.kt$onSaveClicked - ParameterNaming:TelemetryConfigItemList.kt$onSaveClicked ParameterNaming:UsbDevices.kt$onDeviceSelected - ParameterNaming:UserConfigItemList.kt$onSaveClicked ParameterNaming:WelcomeScreen.kt$onGetStarted PreviewAnnotationNaming:LargeFontPreview.kt$LargeFontPreview$LargeFontPreview PreviewPublic:BatteryInfo.kt$BatteryInfoPreview @@ -596,19 +511,18 @@ TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh" + UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit + UnusedParameter:ChannelSettingsItemList.kt$title: String UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule ViewModelForwarding:Main.kt$MainAppBar( viewModel = uIViewModel, navController = navController, onAction = { action -> when (action) { is NodeMenuAction.MoreDetails -> { navController.navigate( NodesRoutes.NodeDetailGraph(action.node.num), { launchSingleTop = true restoreState = true }, ) } is NodeMenuAction.Share -> sharedContact = action.node else -> {} } }, ) - ViewModelForwarding:Main.kt$NavGraph( modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(), uIViewModel = uIViewModel, bluetoothViewModel = bluetoothViewModel, navController = navController, ) ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet) ViewModelForwarding:Main.kt$VersionChecks(uIViewModel) ViewModelForwarding:Message.kt$MessageList( modifier = Modifier.fillMaxSize(), listState = listState, messages = messages, selectedIds = selectedMessageIds, onUnreadChanged = { messageId -> onEvent(MessageScreenEvent.ClearUnreadCount(messageId)) }, onSendReaction = { emoji, id -> onEvent(MessageScreenEvent.SendReaction(emoji, id)) }, viewModel = viewModel, contactKey = contactKey, onReply = { message -> replyingToPacketId = message?.packetId }, onNodeMenuAction = { action -> onEvent(MessageScreenEvent.HandleNodeMenuAction(action)) }, ) - ViewModelForwarding:NodeDetail.kt$NodeDetailContent( node = node, ourNode = ourNode, metricsState = state, lastTracerouteTime = lastTracerouteTime, availableLogs = availableLogs, uiViewModel = uiViewModel, onAction = { action -> handleNodeAction( action = action, uiViewModel = uiViewModel, node = node, navigateToMessages = navigateToMessages, onNavigateUp = onNavigateUp, onNavigate = onNavigate, viewModel = viewModel, ) }, modifier = modifier, ) ViewModelForwarding:NodeScreen.kt$AddContactFAB( modifier = Modifier.animateFloatingActionButton( visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable, alignment = Alignment.BottomEnd, ), model = model, onSharedContactImport = { contact -> model.addSharedContact(contact) }, ) ViewModelInjection:DebugSearch.kt$viewModel WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.* Wrapping:DebugFilters.kt$( Wrapping:DebugFilters.kt$if (filter in filterTexts) { Icon( imageVector = Icons.Filled.Done, contentDescription = stringResource(id = R.string.debug_filter_included), ) } - Wrapping:EditTextPreference.kt$; Wrapping:MQTTRepository.kt$MQTTRepository.<no name provided>$( Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.HandleNodeMenuAction -> { when (val action = event.action) { is NodeMenuAction.DirectMessage -> { val hasPKC = ourNode?.hasPKC == true && action.node.hasPKC val targetChannel = if (hasPKC) { DataPacket.PKC_CHANNEL_INDEX } else { action.node.channel } navigateToMessages("$targetChannel${action.node.user.id}") } is NodeMenuAction.MoreDetails -> navigateToNodeDetails(action.node.num) is NodeMenuAction.Share -> sharedContact = action.node else -> viewModel.handleNodeMenuAction(action) } } is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } } Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$( diff --git a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt index 0f9e5802a..e1f6fae20 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt @@ -47,6 +47,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraphBuilder @@ -153,7 +154,7 @@ fun NavDestination.isConfigRoute(): Boolean = private inline fun NavGraphBuilder.addRadioConfigScreenComposable( navController: NavHostController, routeNameString: String, - crossinline screenContent: @Composable (viewModel: RadioConfigViewModel) -> Unit, + crossinline screenContent: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit, ) { composable( deepLinks = @@ -167,7 +168,7 @@ private inline fun NavGraphBuilder.addRadioConfigScreenCompo val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } val viewModel = hiltViewModel(parentEntry) - screenContent(viewModel) + screenContent(navController, viewModel) } } @@ -306,71 +307,71 @@ enum class ConfigRoute( val route: Route, val icon: ImageVector?, val type: Int = 0, - val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit, + val screenComposable: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit, ) { - USER(R.string.user, SettingsRoutes.User, Icons.Default.Person, 0, { vm -> UserConfigScreen(vm) }), + USER(R.string.user, SettingsRoutes.User, Icons.Default.Person, 0, { nc, vm -> UserConfigScreen(nc, vm) }), CHANNELS( R.string.channels, SettingsRoutes.ChannelConfig, Icons.AutoMirrored.Default.List, 0, - { vm -> ChannelConfigScreen(vm) }, + { nc, vm -> ChannelConfigScreen(nc, vm) }, ), DEVICE( R.string.device, SettingsRoutes.Device, Icons.Default.Router, AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE, - { vm -> DeviceConfigScreen(vm) }, + { nc, vm -> DeviceConfigScreen(nc, vm) }, ), POSITION( R.string.position, SettingsRoutes.Position, Icons.Default.LocationOn, AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE, - { vm -> PositionConfigScreen(vm) }, + { nc, vm -> PositionConfigScreen(nc, vm) }, ), POWER( R.string.power, SettingsRoutes.Power, Icons.Default.Power, AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE, - { vm -> PowerConfigScreen(vm) }, + { nc, vm -> PowerConfigScreen(nc, vm) }, ), NETWORK( R.string.network, SettingsRoutes.Network, Icons.Default.Wifi, AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE, - { vm -> NetworkConfigScreen(vm) }, + { nc, vm -> NetworkConfigScreen(nc, vm) }, ), DISPLAY( R.string.display, SettingsRoutes.Display, Icons.Default.DisplaySettings, AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE, - { vm -> DisplayConfigScreen(vm) }, + { nc, vm -> DisplayConfigScreen(nc, vm) }, ), LORA( R.string.lora, SettingsRoutes.LoRa, Icons.Default.CellTower, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE, - { vm -> LoRaConfigScreen(vm) }, + { nc, vm -> LoRaConfigScreen(nc, vm) }, ), BLUETOOTH( R.string.bluetooth, SettingsRoutes.Bluetooth, Icons.Default.Bluetooth, AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE, - { vm -> BluetoothConfigScreen(vm) }, + { nc, vm -> BluetoothConfigScreen(nc, vm) }, ), SECURITY( R.string.security, SettingsRoutes.Security, Icons.Default.Security, AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE, - { vm -> SecurityConfigScreen(vm) }, + { nc, vm -> SecurityConfigScreen(nc, vm) }, ), ; @@ -397,98 +398,98 @@ enum class ModuleRoute( val route: Route, val icon: ImageVector?, val type: Int = 0, - val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit, + val screenComposable: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit, ) { MQTT( R.string.mqtt, SettingsRoutes.MQTT, Icons.Default.Cloud, AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE, - { vm -> MQTTConfigScreen(vm) }, + { nc, vm -> MQTTConfigScreen(nc, vm) }, ), SERIAL( R.string.serial, SettingsRoutes.Serial, Icons.Default.Usb, AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE, - { vm -> SerialConfigScreen(vm) }, + { nc, vm -> SerialConfigScreen(nc, vm) }, ), EXT_NOTIFICATION( R.string.external_notification, SettingsRoutes.ExtNotification, Icons.Default.Notifications, AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE, - { vm -> ExternalNotificationConfigScreen(vm) }, + { nc, vm -> ExternalNotificationConfigScreen(nc, vm) }, ), STORE_FORWARD( R.string.store_forward, SettingsRoutes.StoreForward, Icons.AutoMirrored.Default.Forward, AdminProtos.AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG_VALUE, - { vm -> StoreForwardConfigScreen(vm) }, + { nc, vm -> StoreForwardConfigScreen(nc, vm) }, ), RANGE_TEST( R.string.range_test, SettingsRoutes.RangeTest, Icons.Default.Speed, AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE, - { vm -> RangeTestConfigScreen(vm) }, + { nc, vm -> RangeTestConfigScreen(nc, vm) }, ), TELEMETRY( R.string.telemetry, SettingsRoutes.Telemetry, Icons.Default.DataUsage, AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE, - { vm -> TelemetryConfigScreen(vm) }, + { nc, vm -> TelemetryConfigScreen(nc, vm) }, ), CANNED_MESSAGE( R.string.canned_message, SettingsRoutes.CannedMessage, Icons.AutoMirrored.Default.Message, AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE, - { vm -> CannedMessageConfigScreen(vm) }, + { nc, vm -> CannedMessageConfigScreen(nc, vm) }, ), AUDIO( R.string.audio, SettingsRoutes.Audio, Icons.AutoMirrored.Default.VolumeUp, AdminProtos.AdminMessage.ModuleConfigType.AUDIO_CONFIG_VALUE, - { vm -> AudioConfigScreen(vm) }, + { nc, vm -> AudioConfigScreen(nc, vm) }, ), REMOTE_HARDWARE( R.string.remote_hardware, SettingsRoutes.RemoteHardware, Icons.Default.SettingsRemote, AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE, - { vm -> RemoteHardwareConfigScreen(vm) }, + { nc, vm -> RemoteHardwareConfigScreen(nc, vm) }, ), NEIGHBOR_INFO( R.string.neighbor_info, SettingsRoutes.NeighborInfo, Icons.Default.People, AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE, - { vm -> NeighborInfoConfigScreen(vm) }, + { nc, vm -> NeighborInfoConfigScreen(nc, vm) }, ), AMBIENT_LIGHTING( R.string.ambient_lighting, SettingsRoutes.AmbientLighting, Icons.Default.LightMode, AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE, - { vm -> AmbientLightingConfigScreen(vm) }, + { nc, vm -> AmbientLightingConfigScreen(nc, vm) }, ), DETECTION_SENSOR( R.string.detection_sensor, SettingsRoutes.DetectionSensor, Icons.Default.Sensors, AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE, - { vm -> DetectionSensorConfigScreen(vm) }, + { nc, vm -> DetectionSensorConfigScreen(nc, vm) }, ), PAXCOUNTER( R.string.paxcounter, SettingsRoutes.Paxcounter, Icons.Default.PermScanWifi, AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE, - { vm -> PaxcounterConfigScreen(vm) }, + { nc, vm -> PaxcounterConfigScreen(nc, vm) }, ), ; diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 5dd84a2f5..f0a823b27 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -133,7 +133,6 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, companion object { fun NavDestination.isTopLevel(): Boolean = listOf>( ContactsRoutes.Contacts::class, - NodesRoutes.Nodes::class, MapRoutes.Map::class, ConnectionsRoutes.Connections::class, ) @@ -356,10 +355,34 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode NodesRoutes.Nodes::class, NodesRoutes.NodeDetail::class, SettingsRoutes.Settings::class, + SettingsRoutes.AmbientLighting::class, + SettingsRoutes.LoRa::class, + SettingsRoutes.Security::class, + SettingsRoutes.Audio::class, + SettingsRoutes.Bluetooth::class, + SettingsRoutes.ChannelConfig::class, + SettingsRoutes.DetectionSensor::class, + SettingsRoutes.Display::class, + SettingsRoutes.Telemetry::class, + SettingsRoutes.Network::class, + SettingsRoutes.Paxcounter::class, + SettingsRoutes.Power::class, + SettingsRoutes.Position::class, + SettingsRoutes.User::class, + SettingsRoutes.StoreForward::class, + SettingsRoutes.MQTT::class, + SettingsRoutes.Serial::class, + SettingsRoutes.ExtNotification::class, + SettingsRoutes.CleanNodeDb::class, + SettingsRoutes.DebugPanel::class, + SettingsRoutes.RangeTest::class, + SettingsRoutes.CannedMessage::class, + SettingsRoutes.RemoteHardware::class, + SettingsRoutes.NeighborInfo::class, ) .none { this.hasRoute(it) } - AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) { + AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: false) { MainAppBar( viewModel = uIViewModel, navController = navController, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index b2885e765..2191fe922 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -186,9 +186,16 @@ fun SettingsScreen( topBar = { MainAppBar( title = stringResource(R.string.bottom_nav_settings), + subtitle = + if (state.isLocal) { + ourNode?.user?.longName + } else { + val remoteName = viewModel.destNode.value?.user?.longName ?: "" + stringResource(R.string.remotely_administrating, remoteName) + }, ourNode = ourNode, isConnected = isConnected, - showNodeChip = ourNode != null && isConnected, + showNodeChip = ourNode != null && isConnected && state.isLocal, canNavigateUp = false, onNavigateUp = {}, actions = {}, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt index 03e7d4366..353e87c38 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AmbientLightingConfigItemList.kt @@ -17,67 +17,50 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val ambientLightingConfig = state.moduleConfig.ambientLighting + val formState = rememberConfigState(initialValue = ambientLightingConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - AmbientLightingConfigItemList( - ambientLightingConfig = state.moduleConfig.ambientLighting, + RadioConfigScreenList( + title = stringResource(id = R.string.ambient_lighting), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { ambientLightingInput -> - val config = moduleConfig { ambientLighting = ambientLightingInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { ambientLighting = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun AmbientLightingConfigItemList( - ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, - enabled: Boolean, - onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var ambientLightingInput by rememberSaveable { mutableStateOf(ambientLightingConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.ambient_lighting_config)) } item { SwitchPreference( title = stringResource(R.string.led_state), - checked = ambientLightingInput.ledState, - enabled = enabled, - onCheckedChange = { ambientLightingInput = ambientLightingInput.copy { ledState = it } }, + checked = formState.value.ledState, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { ledState = it } }, ) } item { HorizontalDivider() } @@ -85,65 +68,41 @@ fun AmbientLightingConfigItemList( item { EditTextPreference( title = stringResource(R.string.current), - value = ambientLightingInput.current, - enabled = enabled, + value = formState.value.current, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { ambientLightingInput = ambientLightingInput.copy { current = it } }, + onValueChanged = { formState.value = formState.value.copy { current = it } }, ) } item { EditTextPreference( title = stringResource(R.string.red), - value = ambientLightingInput.red, - enabled = enabled, + value = formState.value.red, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } }, + onValueChanged = { formState.value = formState.value.copy { red = it } }, ) } item { EditTextPreference( title = stringResource(R.string.green), - value = ambientLightingInput.green, - enabled = enabled, + value = formState.value.green, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } }, + onValueChanged = { formState.value = formState.value.copy { green = it } }, ) } item { EditTextPreference( title = stringResource(R.string.blue), - value = ambientLightingInput.blue, - enabled = enabled, + value = formState.value.blue, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { ambientLightingInput = ambientLightingInput.copy { blue = it } }, - ) - } - - item { - PreferenceFooter( - enabled = enabled && ambientLightingInput != ambientLightingConfig, - onCancelClicked = { - focusManager.clearFocus() - ambientLightingInput = ambientLightingConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(ambientLightingInput) - }, + onValueChanged = { formState.value = formState.value.copy { blue = it } }, ) } } } - -@Preview(showBackground = true) -@Composable -private fun AmbientLightingConfigPreview() { - AmbientLightingConfigItemList( - ambientLightingConfig = ModuleConfigProtos.ModuleConfig.AmbientLightingConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt index aa14696af..637bf90a7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/AudioConfigItemList.kt @@ -17,66 +17,52 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val audioConfig = state.moduleConfig.audio + val formState = rememberConfigState(initialValue = audioConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - AudioConfigItemList( - audioConfig = state.moduleConfig.audio, + RadioConfigScreenList( + title = stringResource(id = R.string.audio), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { audioInput -> - val config = moduleConfig { audio = audioInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { audio = it } viewModel.setModuleConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun AudioConfigItemList(audioConfig: AudioConfig, enabled: Boolean, onSaveClicked: (AudioConfig) -> Unit) { - val focusManager = LocalFocusManager.current - var audioInput by rememberSaveable { mutableStateOf(audioConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.audio_config)) } item { SwitchPreference( title = stringResource(R.string.codec_2_enabled), - checked = audioInput.codec2Enabled, - enabled = enabled, - onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } }, + checked = formState.value.codec2Enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { codec2Enabled = it } }, ) } item { HorizontalDivider() } @@ -84,85 +70,65 @@ fun AudioConfigItemList(audioConfig: AudioConfig, enabled: Boolean, onSaveClicke item { EditTextPreference( title = stringResource(R.string.ptt_pin), - value = audioInput.pttPin, - enabled = enabled, + value = formState.value.pttPin, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { audioInput = audioInput.copy { pttPin = it } }, + onValueChanged = { formState.value = formState.value.copy { pttPin = it } }, ) } item { DropDownPreference( title = stringResource(R.string.codec2_sample_rate), - enabled = enabled, + enabled = state.connected, items = AudioConfig.Audio_Baud.entries .filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED } .map { it to it.name }, - selectedItem = audioInput.bitrate, - onItemSelected = { audioInput = audioInput.copy { bitrate = it } }, + selectedItem = formState.value.bitrate, + onItemSelected = { formState.value = formState.value.copy { bitrate = it } }, ) } - item { Divider() } + item { HorizontalDivider() } item { EditTextPreference( title = stringResource(R.string.i2s_word_select), - value = audioInput.i2SWs, - enabled = enabled, + value = formState.value.i2SWs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { audioInput = audioInput.copy { i2SWs = it } }, + onValueChanged = { formState.value = formState.value.copy { i2SWs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.i2s_data_in), - value = audioInput.i2SSd, - enabled = enabled, + value = formState.value.i2SSd, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { audioInput = audioInput.copy { i2SSd = it } }, + onValueChanged = { formState.value = formState.value.copy { i2SSd = it } }, ) } item { EditTextPreference( title = stringResource(R.string.i2s_data_out), - value = audioInput.i2SDin, - enabled = enabled, + value = formState.value.i2SDin, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { audioInput = audioInput.copy { i2SDin = it } }, + onValueChanged = { formState.value = formState.value.copy { i2SDin = it } }, ) } item { EditTextPreference( title = stringResource(R.string.i2s_clock), - value = audioInput.i2SSck, - enabled = enabled, + value = formState.value.i2SSck, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { audioInput = audioInput.copy { i2SSck = it } }, - ) - } - - item { - PreferenceFooter( - enabled = enabled && audioInput != audioConfig, - onCancelClicked = { - focusManager.clearFocus() - audioInput = audioConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(audioInput) - }, + onValueChanged = { formState.value = formState.value.copy { i2SSck = it } }, ) } } } - -@Preview(showBackground = true) -@Composable -private fun AudioConfigPreview() { - AudioConfigItemList(audioConfig = AudioConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt index 15723cf1e..019e34ac5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/BluetoothConfigItemList.kt @@ -17,68 +17,52 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun BluetoothConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val bluetoothConfig = state.radioConfig.bluetooth + val formState = rememberConfigState(initialValue = bluetoothConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - BluetoothConfigItemList( - bluetoothConfig = state.radioConfig.bluetooth, + RadioConfigScreenList( + title = stringResource(id = R.string.bluetooth), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { bluetoothInput -> - val config = config { bluetooth = bluetoothInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { bluetooth = it } viewModel.setConfig(config) }, - ) -} - -@Composable -fun BluetoothConfigItemList( - bluetoothConfig: BluetoothConfig, - enabled: Boolean, - onSaveClicked: (BluetoothConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var bluetoothInput by rememberSaveable { mutableStateOf(bluetoothConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.bluetooth_config)) } item { SwitchPreference( title = stringResource(R.string.bluetooth_enabled), - checked = bluetoothInput.enabled, - enabled = enabled, - onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -86,13 +70,13 @@ fun BluetoothConfigItemList( item { DropDownPreference( title = stringResource(R.string.pairing_mode), - enabled = enabled, + enabled = state.connected, items = BluetoothConfig.PairingMode.entries .filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED } .map { it to it.name }, - selectedItem = bluetoothInput.mode, - onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } }, + selectedItem = formState.value.mode, + onItemSelected = { formState.value = formState.value.copy { mode = it } }, ) } item { HorizontalDivider() } @@ -100,35 +84,15 @@ fun BluetoothConfigItemList( item { EditTextPreference( title = stringResource(R.string.fixed_pin), - value = bluetoothInput.fixedPin, - enabled = enabled, + value = formState.value.fixedPin, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { if (it.toString().length == 6) { // ensure 6 digits - bluetoothInput = bluetoothInput.copy { fixedPin = it } + formState.value = formState.value.copy { fixedPin = it } } }, ) } - - item { - PreferenceFooter( - enabled = enabled && bluetoothInput != bluetoothConfig, - onCancelClicked = { - focusManager.clearFocus() - bluetoothInput = bluetoothConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(bluetoothInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun BluetoothConfigPreview() { - BluetoothConfigItemList(bluetoothConfig = BluetoothConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt index 434ada785..dbd2948fa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/CannedMessageConfigItemList.kt @@ -17,8 +17,6 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider @@ -27,69 +25,57 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val cannedMessageConfig = state.moduleConfig.cannedMessage + val messages = state.cannedMessageMessages + val formState = rememberConfigState(initialValue = cannedMessageConfig) + var messagesInput by rememberSaveable(messages) { mutableStateOf(messages) } + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - CannedMessageConfigItemList( - messages = state.cannedMessageMessages, - cannedMessageConfig = state.moduleConfig.cannedMessage, + RadioConfigScreenList( + title = stringResource(id = R.string.canned_message), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { messagesInput, cannedMessageInput -> - if (messagesInput != state.cannedMessageMessages) { + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + if (messagesInput != messages) { viewModel.setCannedMessages(messagesInput) } - if (cannedMessageInput != state.moduleConfig.cannedMessage) { - val config = moduleConfig { cannedMessage = cannedMessageInput } + if (formState.value != cannedMessageConfig) { + val config = moduleConfig { cannedMessage = formState.value } viewModel.setModuleConfig(config) } }, - ) -} - -@Composable -fun CannedMessageConfigItemList( - messages: String, - cannedMessageConfig: CannedMessageConfig, - enabled: Boolean, - onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var messagesInput by rememberSaveable { mutableStateOf(messages) } - var cannedMessageInput by rememberSaveable { mutableStateOf(cannedMessageConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.canned_message_config)) } item { SwitchPreference( title = stringResource(R.string.canned_message_enabled), - checked = cannedMessageInput.enabled, - enabled = enabled, - onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -97,9 +83,9 @@ fun CannedMessageConfigItemList( item { SwitchPreference( title = stringResource(R.string.rotary_encoder_1_enabled), - checked = cannedMessageInput.rotary1Enabled, - enabled = enabled, - onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it } }, + checked = formState.value.rotary1Enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { rotary1Enabled = it } }, ) } item { HorizontalDivider() } @@ -107,43 +93,43 @@ fun CannedMessageConfigItemList( item { EditTextPreference( title = stringResource(R.string.gpio_pin_for_rotary_encoder_a_port), - value = cannedMessageInput.inputbrokerPinA, - enabled = enabled, + value = formState.value.inputbrokerPinA, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it } }, + onValueChanged = { formState.value = formState.value.copy { inputbrokerPinA = it } }, ) } item { EditTextPreference( title = stringResource(R.string.gpio_pin_for_rotary_encoder_b_port), - value = cannedMessageInput.inputbrokerPinB, - enabled = enabled, + value = formState.value.inputbrokerPinB, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it } }, + onValueChanged = { formState.value = formState.value.copy { inputbrokerPinB = it } }, ) } item { EditTextPreference( title = stringResource(R.string.gpio_pin_for_rotary_encoder_press_port), - value = cannedMessageInput.inputbrokerPinPress, - enabled = enabled, + value = formState.value.inputbrokerPinPress, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it } }, + onValueChanged = { formState.value = formState.value.copy { inputbrokerPinPress = it } }, ) } item { DropDownPreference( title = stringResource(R.string.generate_input_event_on_press), - enabled = enabled, + enabled = state.connected, items = CannedMessageConfig.InputEventChar.entries .filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED } .map { it to it.name }, - selectedItem = cannedMessageInput.inputbrokerEventPress, - onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it } }, + selectedItem = formState.value.inputbrokerEventPress, + onItemSelected = { formState.value = formState.value.copy { inputbrokerEventPress = it } }, ) } item { HorizontalDivider() } @@ -151,13 +137,13 @@ fun CannedMessageConfigItemList( item { DropDownPreference( title = stringResource(R.string.generate_input_event_on_cw), - enabled = enabled, + enabled = state.connected, items = CannedMessageConfig.InputEventChar.entries .filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED } .map { it to it.name }, - selectedItem = cannedMessageInput.inputbrokerEventCw, - onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it } }, + selectedItem = formState.value.inputbrokerEventCw, + onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCw = it } }, ) } item { HorizontalDivider() } @@ -165,13 +151,13 @@ fun CannedMessageConfigItemList( item { DropDownPreference( title = stringResource(R.string.generate_input_event_on_ccw), - enabled = enabled, + enabled = state.connected, items = CannedMessageConfig.InputEventChar.entries .filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED } .map { it to it.name }, - selectedItem = cannedMessageInput.inputbrokerEventCcw, - onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it } }, + selectedItem = formState.value.inputbrokerEventCcw, + onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCcw = it } }, ) } item { HorizontalDivider() } @@ -179,9 +165,9 @@ fun CannedMessageConfigItemList( item { SwitchPreference( title = stringResource(R.string.up_down_select_input_enabled), - checked = cannedMessageInput.updown1Enabled, - enabled = enabled, - onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it } }, + checked = formState.value.updown1Enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { updown1Enabled = it } }, ) } item { HorizontalDivider() } @@ -189,23 +175,23 @@ fun CannedMessageConfigItemList( item { EditTextPreference( title = stringResource(R.string.allow_input_source), - value = cannedMessageInput.allowInputSource, + value = formState.value.allowInputSource, maxSize = 63, // allow_input_source max_size:16 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { cannedMessageInput = cannedMessageInput.copy { allowInputSource = it } }, + onValueChanged = { formState.value = formState.value.copy { allowInputSource = it } }, ) } item { SwitchPreference( title = stringResource(R.string.send_bell), - checked = cannedMessageInput.sendBell, - enabled = enabled, - onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { sendBell = it } }, + checked = formState.value.sendBell, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { sendBell = it } }, ) } item { HorizontalDivider() } @@ -215,7 +201,7 @@ fun CannedMessageConfigItemList( title = stringResource(R.string.messages), value = messagesInput, maxSize = 200, // messages max_size:201 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), @@ -223,31 +209,5 @@ fun CannedMessageConfigItemList( onValueChanged = { messagesInput = it }, ) } - - item { - PreferenceFooter( - enabled = enabled && cannedMessageInput != cannedMessageConfig || messagesInput != messages, - onCancelClicked = { - focusManager.clearFocus() - messagesInput = messages - cannedMessageInput = cannedMessageConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(messagesInput, cannedMessageInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun CannedMessageConfigPreview() { - CannedMessageConfigItemList( - messages = "", - cannedMessageConfig = CannedMessageConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = { _, _ -> }, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt index b09a7336e..b6d0153ee 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt @@ -47,6 +47,7 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -68,6 +69,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ChannelProtos.ChannelSettings import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig import com.geeksville.mesh.channelSettings @@ -168,7 +170,7 @@ fun ChannelSelection( } @Composable -fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun ChannelConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() if (state.responseState.isWaiting()) { @@ -176,6 +178,8 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { } ChannelSettingsItemList( + title = stringResource(id = R.string.channels), + onBack = { navController.popBackStack() }, settingsList = state.channelList, loraConfig = state.radioConfig.lora, maxChannels = viewModel.maxChannels, @@ -188,6 +192,8 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun ChannelSettingsItemList( + title: String, + onBack: () -> Unit, settingsList: List, loraConfig: LoRaConfig, maxChannels: Int = 8, @@ -243,104 +249,116 @@ private fun ChannelSettingsItemList( ChannelLegendDialog(fwVersion) { showChannelLegendDialog = false } } - Box(modifier = Modifier.fillMaxSize().clickable(onClick = {}, enabled = false)) { - Column { - ChannelsConfigHeader( - frequency = - if (loraConfig.overrideFrequency != 0f) { - loraConfig.overrideFrequency - } else { - primaryChannel.radioFreq - }, - slot = - if (loraConfig.channelNum != 0) { - loraConfig.channelNum - } else { - primaryChannel.channelNum - }, - ) - Text( - text = stringResource(R.string.press_and_drag), - fontSize = 11.sp, - modifier = Modifier.padding(start = 16.dp), - ) - - ChannelLegend { showChannelLegendDialog = true } - - val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList()) - - LazyColumn( - modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current), - state = listState, - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - dragDropItemsIndexed(items = settingsListInput, dragDropState = dragDropState) { - index, - channel, - isDragging, - -> - ChannelCard( - index = index, - title = channel.name.ifEmpty { modemPresetName }, - enabled = enabled, - channelSettings = channel, - loraConfig = loraConfig, - onEditClick = { showEditChannelDialog = index }, - onDeleteClick = { settingsListInput.removeAt(index) }, - sharesLocation = locationChannel == index, - ) - } - item { - PreferenceFooter( - enabled = enabled && isEditing, - negativeText = R.string.cancel, - onNegativeClicked = { - focusManager.clearFocus() - settingsListInput.clear() - settingsListInput.addAll(settingsList) - }, - positiveText = R.string.send, - onPositiveClicked = { - focusManager.clearFocus() - onPositiveClicked(settingsListInput) - }, - ) + Scaffold( + floatingActionButton = { + if (maxChannels > settingsListInput.size) { + FloatingActionButton( + onClick = { + if (maxChannels > settingsListInput.size) { + settingsListInput.add(channelSettings { psk = Channel.default.settings.psk }) + showEditChannelDialog = settingsListInput.lastIndex + } + }, + modifier = Modifier.padding(16.dp), + ) { + Icon(Icons.TwoTone.Add, stringResource(R.string.add)) } } - } + }, + ) { innerPadding -> + Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + Column { + ChannelsConfigHeader( + frequency = + if (loraConfig.overrideFrequency != 0f) { + loraConfig.overrideFrequency + } else { + primaryChannel.radioFreq + }, + slot = + if (loraConfig.channelNum != 0) { + loraConfig.channelNum + } else { + primaryChannel.channelNum + }, + ) + Text( + text = stringResource(R.string.press_and_drag), + fontSize = 11.sp, + modifier = Modifier.padding(start = 16.dp), + ) - AnimatedVisibility( - visible = maxChannels > settingsListInput.size, - modifier = Modifier.align(Alignment.BottomEnd), - enter = - slideInHorizontally( - initialOffsetX = { it }, - animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing), - ), - exit = - slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing), - ), - ) { - FloatingActionButton( - onClick = { - if (maxChannels > settingsListInput.size) { - settingsListInput.add(channelSettings { psk = Channel.default.settings.psk }) - showEditChannelDialog = settingsListInput.lastIndex + ChannelLegend { showChannelLegendDialog = true } + + val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList()) + + LazyColumn( + modifier = + Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current), + state = listState, + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + dragDropItemsIndexed(items = settingsListInput, dragDropState = dragDropState) { + index, + channel, + isDragging, + -> + ChannelCard( + index = index, + title = channel.name.ifEmpty { modemPresetName }, + enabled = enabled, + channelSettings = channel, + loraConfig = loraConfig, + onEditClick = { showEditChannelDialog = index }, + onDeleteClick = { settingsListInput.removeAt(index) }, + sharesLocation = locationChannel == index, + ) } - }, - modifier = Modifier.padding(16.dp), - ) { - Icon(Icons.TwoTone.Add, stringResource(R.string.add)) + item { Spacer(modifier = Modifier.weight(1f)) } + item { + PreferenceFooter( + enabled = enabled && isEditing, + negativeText = R.string.cancel, + onNegativeClicked = { + focusManager.clearFocus() + settingsListInput.clear() + settingsListInput.addAll(settingsList) + }, + positiveText = R.string.send, + onPositiveClicked = { + focusManager.clearFocus() + onPositiveClicked(settingsListInput) + }, + ) + } + } } + + AnimatedVisibility( + visible = maxChannels > settingsListInput.size, + modifier = Modifier.align(Alignment.BottomEnd), + enter = + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing), + ), + exit = + slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing), + ), + ) {} } } } @Composable private fun ChannelsConfigHeader(frequency: Float, slot: Int) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { PreferenceCategory(text = stringResource(R.string.channels)) Column { Text(text = "${stringResource(R.string.freq)}: ${frequency}MHz", fontSize = 11.sp) @@ -380,6 +398,8 @@ private fun determineLocationSharingChannel(firmwareVersion: DeviceVersion, sett @Composable private fun ChannelSettingsPreview() { ChannelSettingsItemList( + title = "Channels", + onBack = {}, settingsList = listOf( channelSettings { diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ConfigState.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ConfigState.kt new file mode 100644 index 000000000..62a0498d5 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ConfigState.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.settings.radio.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.google.protobuf.MessageLite + +/** + * A state holder for managing config data within a Composable. + * + * This class encapsulates the common logic for handling editable state that is derived from an initial value. It tracks + * whether the current value has been modified ("dirty"), and provides simple methods to save the changes or reset to + * the initial state. + * + * @param T The type of the data being managed, typically a Protobuf message. + * @property initialValue The original, unmodified value of the config data. + */ +class ConfigState(private val initialValue: T) { + var value by mutableStateOf(initialValue) + + val isDirty: Boolean + get() = value != initialValue + + fun reset() { + value = initialValue + } + + companion object { + fun saver(initialValue: T): Saver, ByteArray> = Saver( + save = { it.value.toByteArray() }, + restore = { + ConfigState(initialValue).apply { + @Suppress("UNCHECKED_CAST") + value = initialValue.parserForType.parseFrom(it) as T + } + }, + ) + } +} + +/** + * Creates and remembers a [ConfigState] instance, correctly handling process death and recomposition. When the + * `initialValue` changes, the config state will be reset. + * + * @param initialValue The initial value to populate the config with. The config will be reset if this value changes + * across recompositions. + */ +@Composable +fun rememberConfigState(initialValue: T): ConfigState = + rememberSaveable(initialValue, saver = ConfigState.saver(initialValue)) { ConfigState(initialValue) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt index 9f6e84365..3e680d42c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DetectionSensorConfigItemList.kt @@ -17,72 +17,55 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val detectionSensorConfig = state.moduleConfig.detectionSensor + val formState = rememberConfigState(initialValue = detectionSensorConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - DetectionSensorConfigItemList( - detectionSensorConfig = state.moduleConfig.detectionSensor, + RadioConfigScreenList( + title = stringResource(id = R.string.detection_sensor), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { detectionSensorInput -> - val config = moduleConfig { detectionSensor = detectionSensorInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { detectionSensor = it } viewModel.setModuleConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun DetectionSensorConfigItemList( - detectionSensorConfig: ModuleConfig.DetectionSensorConfig, - enabled: Boolean, - onSaveClicked: (ModuleConfig.DetectionSensorConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var detectionSensorInput by rememberSaveable { mutableStateOf(detectionSensorConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.detection_sensor_config)) } item { SwitchPreference( title = stringResource(R.string.detection_sensor_enabled), - checked = detectionSensorInput.enabled, - enabled = enabled, - onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -90,29 +73,29 @@ fun DetectionSensorConfigItemList( item { EditTextPreference( title = stringResource(R.string.minimum_broadcast_seconds), - value = detectionSensorInput.minimumBroadcastSecs, - enabled = enabled, + value = formState.value.minimumBroadcastSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { minimumBroadcastSecs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.state_broadcast_seconds), - value = detectionSensorInput.stateBroadcastSecs, - enabled = enabled, + value = formState.value.stateBroadcastSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { stateBroadcastSecs = it } }, ) } item { SwitchPreference( title = stringResource(R.string.send_bell_with_alert_message), - checked = detectionSensorInput.sendBell, - enabled = enabled, - onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { sendBell = it } }, + checked = formState.value.sendBell, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { sendBell = it } }, ) } item { HorizontalDivider() } @@ -120,37 +103,37 @@ fun DetectionSensorConfigItemList( item { EditTextPreference( title = stringResource(R.string.friendly_name), - value = detectionSensorInput.name, + value = formState.value.name, maxSize = 19, // name max_size:20 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { detectionSensorInput = detectionSensorInput.copy { name = it } }, + onValueChanged = { formState.value = formState.value.copy { name = it } }, ) } item { EditTextPreference( title = stringResource(R.string.gpio_pin_to_monitor), - value = detectionSensorInput.monitorPin, - enabled = enabled, + value = formState.value.monitorPin, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { detectionSensorInput = detectionSensorInput.copy { monitorPin = it } }, + onValueChanged = { formState.value = formState.value.copy { monitorPin = it } }, ) } item { DropDownPreference( title = stringResource(R.string.detection_trigger_type), - enabled = enabled, + enabled = state.connected, items = ModuleConfig.DetectionSensorConfig.TriggerType.entries .filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED } .map { it to it.name }, - selectedItem = detectionSensorInput.detectionTriggerType, - onItemSelected = { detectionSensorInput = detectionSensorInput.copy { detectionTriggerType = it } }, + selectedItem = formState.value.detectionTriggerType, + onItemSelected = { formState.value = formState.value.copy { detectionTriggerType = it } }, ) } item { HorizontalDivider() } @@ -158,35 +141,11 @@ fun DetectionSensorConfigItemList( item { SwitchPreference( title = stringResource(R.string.use_input_pullup_mode), - checked = detectionSensorInput.usePullup, - enabled = enabled, - onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { usePullup = it } }, + checked = formState.value.usePullup, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { usePullup = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && detectionSensorInput != detectionSensorConfig, - onCancelClicked = { - focusManager.clearFocus() - detectionSensorInput = detectionSensorConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(detectionSensorInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun DetectionSensorConfigPreview() { - DetectionSensorConfigItemList( - detectionSensorConfig = ModuleConfig.DetectionSensorConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt index 3f5ba20e3..33b3be14c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt @@ -20,9 +20,7 @@ package com.geeksville.mesh.ui.settings.radio.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog @@ -46,16 +44,15 @@ import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @@ -91,24 +88,135 @@ private val DeviceConfig.RebroadcastMode.description: Int } @Composable -fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) + val deviceConfig = state.radioConfig.device + val formState = rememberConfigState(initialValue = deviceConfig) + var selectedRole by rememberSaveable { mutableStateOf(formState.value.role) } + val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER) + if (selectedRole != formState.value.role) { + if (selectedRole in infrastructureRoles) { + RouterRoleConfirmationDialog( + onDismiss = { selectedRole = formState.value.role }, + onConfirm = { formState.value = formState.value.copy { role = selectedRole } }, + ) + } else { + formState.value = formState.value.copy { role = selectedRole } + } } - - DeviceConfigItemList( - deviceConfig = state.radioConfig.device, + val focusManager = LocalFocusManager.current + RadioConfigScreenList( + title = stringResource(id = R.string.device), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { deviceInput -> - val config = config { device = deviceInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { device = it } viewModel.setConfig(config) }, - ) + ) { + item { PreferenceCategory(text = stringResource(R.string.options)) } + item { + DropDownPreference( + title = stringResource(R.string.role), + enabled = state.connected, + selectedItem = formState.value.role, + onItemSelected = { selectedRole = it }, + summary = stringResource(id = formState.value.role.description), + ) + } + item { HorizontalDivider() } + item { + DropDownPreference( + title = stringResource(R.string.rebroadcast_mode), + enabled = state.connected, + selectedItem = formState.value.rebroadcastMode, + onItemSelected = { formState.value = formState.value.copy { rebroadcastMode = it } }, + summary = stringResource(id = formState.value.rebroadcastMode.description), + ) + } + item { HorizontalDivider() } + item { + EditTextPreference( + title = stringResource(R.string.nodeinfo_broadcast_interval), + value = formState.value.nodeInfoBroadcastSecs, + enabled = state.connected, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { formState.value = formState.value.copy { nodeInfoBroadcastSecs = it } }, + ) + } + item { PreferenceCategory(text = stringResource(R.string.hardware)) } + item { + SwitchPreference( + title = stringResource(R.string.double_tap_as_button_press), + summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary), + checked = formState.value.doubleTapAsButtonPress, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { doubleTapAsButtonPress = it } }, + ) + } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.triple_click_adhoc_ping), + summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary), + checked = !formState.value.disableTripleClick, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { disableTripleClick = !it } }, + ) + } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.led_heartbeat), + summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary), + checked = !formState.value.ledHeartbeatDisabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { ledHeartbeatDisabled = !it } }, + ) + } + item { HorizontalDivider() } + + item { PreferenceCategory(text = stringResource(R.string.debug)) } + item { + EditTextPreference( + title = stringResource(R.string.time_zone), + value = formState.value.tzdef, + summary = stringResource(id = R.string.config_device_tzdef_summary), + maxSize = 64, // tzdef max_size:65 + enabled = state.connected, + isError = false, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { formState.value = formState.value.copy { tzdef = it } }, + ) + } + + item { PreferenceCategory(text = stringResource(R.string.gpio)) } + item { + EditTextPreference( + title = stringResource(R.string.button_gpio), + value = formState.value.buttonGpio, + enabled = state.connected, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { formState.value = formState.value.copy { buttonGpio = it } }, + ) + } + item { + EditTextPreference( + title = stringResource(R.string.buzzer_gpio), + value = formState.value.buzzerGpio, + enabled = state.connected, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { formState.value = formState.value.copy { buzzerGpio = it } }, + ) + } + } } -@Suppress("LongMethod") @Composable fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { val dialogTitle = stringResource(R.string.are_you_sure) @@ -141,140 +249,3 @@ fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }, ) } - -@Suppress("LongMethod") -@Composable -fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveClicked: (DeviceConfig) -> Unit) { - val focusManager = LocalFocusManager.current - var deviceInput by rememberSaveable { mutableStateOf(deviceConfig) } - var selectedRole by rememberSaveable { mutableStateOf(deviceInput.role) } - val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER) - if (selectedRole != deviceInput.role) { - if (selectedRole in infrastructureRoles) { - RouterRoleConfirmationDialog( - onDismiss = { selectedRole = deviceInput.role }, - onConfirm = { deviceInput = deviceInput.copy { role = selectedRole } }, - ) - } else { - deviceInput = deviceInput.copy { role = selectedRole } - } - } - LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.options)) } - item { - DropDownPreference( - title = stringResource(R.string.role), - enabled = enabled, - selectedItem = deviceInput.role, - onItemSelected = { selectedRole = it }, - summary = stringResource(id = deviceInput.role.description), - ) - } - item { HorizontalDivider() } - item { - DropDownPreference( - title = stringResource(R.string.rebroadcast_mode), - enabled = enabled, - selectedItem = deviceInput.rebroadcastMode, - onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } }, - summary = stringResource(id = deviceInput.rebroadcastMode.description), - ) - } - item { HorizontalDivider() } - item { - EditTextPreference( - title = stringResource(R.string.nodeinfo_broadcast_interval), - value = deviceInput.nodeInfoBroadcastSecs, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it } }, - ) - } - item { PreferenceCategory(text = stringResource(R.string.hardware)) } - item { - SwitchPreference( - title = stringResource(R.string.double_tap_as_button_press), - summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary), - checked = deviceInput.doubleTapAsButtonPress, - enabled = enabled, - onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } }, - ) - } - item { HorizontalDivider() } - item { - SwitchPreference( - title = stringResource(R.string.triple_click_adhoc_ping), - summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary), - checked = !deviceInput.disableTripleClick, - enabled = enabled, - onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = !it } }, - ) - } - item { HorizontalDivider() } - item { - SwitchPreference( - title = stringResource(R.string.led_heartbeat), - summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary), - checked = !deviceInput.ledHeartbeatDisabled, - enabled = enabled, - onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = !it } }, - ) - } - item { HorizontalDivider() } - - item { PreferenceCategory(text = stringResource(R.string.debug)) } - item { - EditTextPreference( - title = stringResource(R.string.time_zone), - value = deviceInput.tzdef, - summary = stringResource(id = R.string.config_device_tzdef_summary), - maxSize = 64, // tzdef max_size:65 - enabled = enabled, - isError = false, - keyboardOptions = - KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { tzdef = it } }, - ) - } - - item { PreferenceCategory(text = stringResource(R.string.gpio)) } - item { - EditTextPreference( - title = stringResource(R.string.button_gpio), - value = deviceInput.buttonGpio, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } }, - ) - } - item { - EditTextPreference( - title = stringResource(R.string.buzzer_gpio), - value = deviceInput.buzzerGpio, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } }, - ) - } - item { - PreferenceFooter( - enabled = enabled && deviceInput != deviceConfig, - onCancelClicked = { - focusManager.clearFocus() - deviceInput = deviceConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(deviceInput) - }, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun DeviceConfigPreview() { - DeviceConfigItemList(deviceConfig = DeviceConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt index b9f915cfc..15a27f2bd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt @@ -17,65 +17,52 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val displayConfig = state.radioConfig.display + val formState = rememberConfigState(initialValue = displayConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - DisplayConfigItemList( - displayConfig = state.radioConfig.display, + RadioConfigScreenList( + title = stringResource(id = R.string.display), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { displayInput -> - val config = config { display = displayInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { display = it } viewModel.setConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSaveClicked: (DisplayConfig) -> Unit) { - val focusManager = LocalFocusManager.current - var displayInput by rememberSaveable { mutableStateOf(displayConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.display_config)) } item { SwitchPreference( title = stringResource(R.string.always_point_north), summary = stringResource(id = R.string.config_display_compass_north_top_summary), - checked = displayInput.compassNorthTop, - enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } }, + checked = formState.value.compassNorthTop, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { compassNorthTop = it } }, ) } item { HorizontalDivider() } @@ -83,9 +70,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave SwitchPreference( title = stringResource(R.string.use_12h_format), summary = stringResource(R.string.display_time_in_12h_format), - enabled = enabled, - checked = displayInput.use12HClock, - onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } }, + enabled = state.connected, + checked = formState.value.use12HClock, + onCheckedChange = { formState.value = formState.value.copy { use12HClock = it } }, ) } item { HorizontalDivider() } @@ -93,9 +80,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave SwitchPreference( title = stringResource(R.string.bold_heading), summary = stringResource(id = R.string.config_display_heading_bold_summary), - checked = displayInput.headingBold, - enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { headingBold = it } }, + checked = formState.value.headingBold, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { headingBold = it } }, ) } item { HorizontalDivider() } @@ -103,13 +90,13 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave DropDownPreference( title = stringResource(R.string.display_units), summary = stringResource(id = R.string.config_display_units_summary), - enabled = enabled, + enabled = state.connected, items = DisplayConfig.DisplayUnits.entries .filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED } .map { it to it.name }, - selectedItem = displayInput.units, - onItemSelected = { displayInput = displayInput.copy { units = it } }, + selectedItem = formState.value.units, + onItemSelected = { formState.value = formState.value.copy { units = it } }, ) } item { HorizontalDivider() } @@ -119,10 +106,10 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave EditTextPreference( title = stringResource(R.string.screen_on_for), summary = stringResource(id = R.string.config_display_screen_on_secs_summary), - value = displayInput.screenOnSecs, - enabled = enabled, + value = formState.value.screenOnSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { screenOnSecs = it } }, ) } item { HorizontalDivider() } @@ -131,10 +118,10 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave EditTextPreference( title = stringResource(R.string.carousel_interval), summary = stringResource(id = R.string.config_display_auto_screen_carousel_secs_summary), - value = displayInput.autoScreenCarouselSecs, - enabled = enabled, + value = formState.value.autoScreenCarouselSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { autoScreenCarouselSecs = it } }, ) } item { HorizontalDivider() } @@ -142,9 +129,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave SwitchPreference( title = stringResource(R.string.wake_on_tap_or_motion), summary = stringResource(id = R.string.config_display_wake_on_tap_or_motion_summary), - checked = displayInput.wakeOnTapOrMotion, - enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } }, + checked = formState.value.wakeOnTapOrMotion, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { wakeOnTapOrMotion = it } }, ) } item { HorizontalDivider() } @@ -152,9 +139,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave SwitchPreference( title = stringResource(R.string.flip_screen), summary = stringResource(id = R.string.config_display_flip_screen_summary), - checked = displayInput.flipScreen, - enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } }, + checked = formState.value.flipScreen, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { flipScreen = it } }, ) } item { HorizontalDivider() } @@ -162,13 +149,13 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave DropDownPreference( title = stringResource(R.string.display_mode), summary = stringResource(id = R.string.config_display_displaymode_summary), - enabled = enabled, + enabled = state.connected, items = DisplayConfig.DisplayMode.entries .filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED } .map { it to it.name }, - selectedItem = displayInput.displaymode, - onItemSelected = { displayInput = displayInput.copy { displaymode = it } }, + selectedItem = formState.value.displaymode, + onItemSelected = { formState.value = formState.value.copy { displaymode = it } }, ) } item { HorizontalDivider() } @@ -176,48 +163,28 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave DropDownPreference( title = stringResource(R.string.oled_type), summary = stringResource(id = R.string.config_display_oled_summary), - enabled = enabled, + enabled = state.connected, items = DisplayConfig.OledType.entries .filter { it != DisplayConfig.OledType.UNRECOGNIZED } .map { it to it.name }, - selectedItem = displayInput.oled, - onItemSelected = { displayInput = displayInput.copy { oled = it } }, + selectedItem = formState.value.oled, + onItemSelected = { formState.value = formState.value.copy { oled = it } }, ) } item { HorizontalDivider() } item { DropDownPreference( title = stringResource(R.string.compass_orientation), - enabled = enabled, + enabled = state.connected, items = DisplayConfig.CompassOrientation.entries .filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED } .map { it to it.name }, - selectedItem = displayInput.compassOrientation, - onItemSelected = { displayInput = displayInput.copy { compassOrientation = it } }, + selectedItem = formState.value.compassOrientation, + onItemSelected = { formState.value = formState.value.copy { compassOrientation = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && displayInput != displayConfig, - onCancelClicked = { - focusManager.clearFocus() - displayInput = displayConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(displayInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun DisplayConfigPreview() { - DisplayConfigItemList(displayConfig = DisplayConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt index d8ba38ba8..3cbd75eee 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ExternalNotificationConfigItemList.kt @@ -17,8 +17,6 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider @@ -27,80 +25,69 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.common.components.TextDividerPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun ExternalNotificationConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val extNotificationConfig = state.moduleConfig.externalNotification + val ringtone = state.ringtone + val formState = rememberConfigState(initialValue = extNotificationConfig) + var ringtoneInput by rememberSaveable(ringtone) { mutableStateOf(ringtone) } + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - ExternalNotificationConfigItemList( - ringtone = state.ringtone, - extNotificationConfig = state.moduleConfig.externalNotification, + RadioConfigScreenList( + title = stringResource(id = R.string.external_notification), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { ringtoneInput, extNotificationInput -> - if (ringtoneInput != state.ringtone) { + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + if (ringtoneInput != ringtone) { viewModel.setRingtone(ringtoneInput) } - if (extNotificationInput != state.moduleConfig.externalNotification) { - val config = moduleConfig { externalNotification = extNotificationInput } + if (formState.value != extNotificationConfig) { + val config = moduleConfig { externalNotification = formState.value } viewModel.setModuleConfig(config) } }, - ) -} - -@Composable -fun ExternalNotificationConfigItemList( - ringtone: String, - extNotificationConfig: ExternalNotificationConfig, - enabled: Boolean, - onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var ringtoneInput by rememberSaveable { mutableStateOf(ringtone) } - var externalNotificationInput by rememberSaveable { mutableStateOf(extNotificationConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.external_notification_config)) } item { SwitchPreference( title = stringResource(R.string.external_notification_enabled), - checked = externalNotificationInput.enabled, - enabled = enabled, - onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } - item { TextDividerPreference(stringResource(R.string.notifications_on_message_receipt), enabled = enabled) } + item { + TextDividerPreference(stringResource(R.string.notifications_on_message_receipt), enabled = state.connected) + } item { SwitchPreference( title = stringResource(R.string.alert_message_led), - checked = externalNotificationInput.alertMessage, - enabled = enabled, - onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertMessage = it } }, + checked = formState.value.alertMessage, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertMessage = it } }, ) } item { HorizontalDivider() } @@ -108,11 +95,9 @@ fun ExternalNotificationConfigItemList( item { SwitchPreference( title = stringResource(R.string.alert_message_buzzer), - checked = externalNotificationInput.alertMessageBuzzer, - enabled = enabled, - onCheckedChange = { - externalNotificationInput = externalNotificationInput.copy { alertMessageBuzzer = it } - }, + checked = formState.value.alertMessageBuzzer, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertMessageBuzzer = it } }, ) } item { HorizontalDivider() } @@ -120,22 +105,25 @@ fun ExternalNotificationConfigItemList( item { SwitchPreference( title = stringResource(R.string.alert_message_vibra), - checked = externalNotificationInput.alertMessageVibra, - enabled = enabled, - onCheckedChange = { - externalNotificationInput = externalNotificationInput.copy { alertMessageVibra = it } - }, + checked = formState.value.alertMessageVibra, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertMessageVibra = it } }, ) } - item { TextDividerPreference(stringResource(R.string.notifications_on_alert_bell_receipt), enabled = enabled) } + item { + TextDividerPreference( + stringResource(R.string.notifications_on_alert_bell_receipt), + enabled = state.connected, + ) + } item { SwitchPreference( title = stringResource(R.string.alert_bell_led), - checked = externalNotificationInput.alertBell, - enabled = enabled, - onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertBell = it } }, + checked = formState.value.alertBell, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertBell = it } }, ) } item { HorizontalDivider() } @@ -143,11 +131,9 @@ fun ExternalNotificationConfigItemList( item { SwitchPreference( title = stringResource(R.string.alert_bell_buzzer), - checked = externalNotificationInput.alertBellBuzzer, - enabled = enabled, - onCheckedChange = { - externalNotificationInput = externalNotificationInput.copy { alertBellBuzzer = it } - }, + checked = formState.value.alertBellBuzzer, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertBellBuzzer = it } }, ) } item { HorizontalDivider() } @@ -155,11 +141,9 @@ fun ExternalNotificationConfigItemList( item { SwitchPreference( title = stringResource(R.string.alert_bell_vibra), - checked = externalNotificationInput.alertBellVibra, - enabled = enabled, - onCheckedChange = { - externalNotificationInput = externalNotificationInput.copy { alertBellVibra = it } - }, + checked = formState.value.alertBellVibra, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { alertBellVibra = it } }, ) } item { HorizontalDivider() } @@ -167,20 +151,20 @@ fun ExternalNotificationConfigItemList( item { EditTextPreference( title = stringResource(R.string.output_led_gpio), - value = externalNotificationInput.output, - enabled = enabled, + value = formState.value.output, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { externalNotificationInput = externalNotificationInput.copy { output = it } }, + onValueChanged = { formState.value = formState.value.copy { output = it } }, ) } - if (externalNotificationInput.output != 0) { + if (formState.value.output != 0) { item { SwitchPreference( title = stringResource(R.string.output_led_active_high), - checked = externalNotificationInput.active, - enabled = enabled, - onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { active = it } }, + checked = formState.value.active, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { active = it } }, ) } } @@ -189,20 +173,20 @@ fun ExternalNotificationConfigItemList( item { EditTextPreference( title = stringResource(R.string.output_buzzer_gpio), - value = externalNotificationInput.outputBuzzer, - enabled = enabled, + value = formState.value.outputBuzzer, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it } }, + onValueChanged = { formState.value = formState.value.copy { outputBuzzer = it } }, ) } - if (externalNotificationInput.outputBuzzer != 0) { + if (formState.value.outputBuzzer != 0) { item { SwitchPreference( title = stringResource(R.string.use_pwm_buzzer), - checked = externalNotificationInput.usePwm, - enabled = enabled, - onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { usePwm = it } }, + checked = formState.value.usePwm, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { usePwm = it } }, ) } } @@ -211,30 +195,30 @@ fun ExternalNotificationConfigItemList( item { EditTextPreference( title = stringResource(R.string.output_vibra_gpio), - value = externalNotificationInput.outputVibra, - enabled = enabled, + value = formState.value.outputVibra, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputVibra = it } }, + onValueChanged = { formState.value = formState.value.copy { outputVibra = it } }, ) } item { EditTextPreference( title = stringResource(R.string.output_duration_milliseconds), - value = externalNotificationInput.outputMs, - enabled = enabled, + value = formState.value.outputMs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputMs = it } }, + onValueChanged = { formState.value = formState.value.copy { outputMs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.nag_timeout_seconds), - value = externalNotificationInput.nagTimeout, - enabled = enabled, + value = formState.value.nagTimeout, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { externalNotificationInput = externalNotificationInput.copy { nagTimeout = it } }, + onValueChanged = { formState.value = formState.value.copy { nagTimeout = it } }, ) } @@ -243,7 +227,7 @@ fun ExternalNotificationConfigItemList( title = stringResource(R.string.ringtone), value = ringtoneInput, maxSize = 230, // ringtone max_size:231 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), @@ -255,39 +239,11 @@ fun ExternalNotificationConfigItemList( item { SwitchPreference( title = stringResource(R.string.use_i2s_as_buzzer), - checked = externalNotificationInput.useI2SAsBuzzer, - enabled = enabled, - onCheckedChange = { - externalNotificationInput = externalNotificationInput.copy { useI2SAsBuzzer = it } - }, + checked = formState.value.useI2SAsBuzzer, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { useI2SAsBuzzer = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && externalNotificationInput != extNotificationConfig || ringtoneInput != ringtone, - onCancelClicked = { - focusManager.clearFocus() - ringtoneInput = ringtone - externalNotificationInput = extNotificationConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(ringtoneInput, externalNotificationInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun ExternalNotificationConfigPreview() { - ExternalNotificationConfigItemList( - ringtone = "", - extNotificationConfig = ExternalNotificationConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = { _, _ -> }, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt index 028ce1c30..90a9c8cdc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt @@ -17,30 +17,24 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ChannelProtos.ChannelSettings +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel @@ -50,73 +44,61 @@ import org.meshtastic.core.model.numChannels import org.meshtastic.core.strings.R @Composable -fun LoRaConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val loraConfig = state.radioConfig.lora + val primarySettings = state.channelList.getOrNull(0) ?: return + val formState = rememberConfigState(initialValue = loraConfig) - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } + val primaryChannel by remember(formState.value) { mutableStateOf(Channel(primarySettings, formState.value)) } + val focusManager = LocalFocusManager.current - LoRaConfigItemList( - loraConfig = state.radioConfig.lora, - primarySettings = state.channelList.getOrNull(0) ?: return, + RadioConfigScreenList( + title = stringResource(id = R.string.lora), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { loraInput -> - val config = config { lora = loraInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { lora = it } viewModel.setConfig(config) }, - hasPaFan = viewModel.hasPaFan, - ) -} - -@Suppress("LongMethod") -@Composable -fun LoRaConfigItemList( - loraConfig: LoRaConfig, - primarySettings: ChannelSettings, - enabled: Boolean, - onSaveClicked: (LoRaConfig) -> Unit, - hasPaFan: Boolean = false, -) { - val focusManager = LocalFocusManager.current - var loraInput by rememberSaveable { mutableStateOf(loraConfig) } - val primaryChannel by remember(loraInput) { mutableStateOf(Channel(primarySettings, loraInput)) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.options)) } item { DropDownPreference( title = stringResource(R.string.region_frequency_plan), summary = stringResource(id = R.string.config_lora_region_summary), - enabled = enabled, + enabled = state.connected, items = RegionInfo.entries.map { it.regionCode to it.description }, - selectedItem = loraInput.region, - onItemSelected = { loraInput = loraInput.copy { region = it } }, + selectedItem = formState.value.region, + onItemSelected = { formState.value = formState.value.copy { region = it } }, ) } item { HorizontalDivider() } item { SwitchPreference( title = stringResource(R.string.use_modem_preset), - checked = loraInput.usePreset, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { usePreset = it } }, + checked = formState.value.usePreset, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { usePreset = it } }, ) } item { HorizontalDivider() } - if (loraInput.usePreset) { + if (formState.value.usePreset) { item { DropDownPreference( title = stringResource(R.string.modem_preset), summary = stringResource(id = R.string.config_lora_modem_preset_summary), - enabled = enabled && loraInput.usePreset, + enabled = state.connected && formState.value.usePreset, items = LoRaConfig.ModemPreset.entries .filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED } .map { it to it.name }, - selectedItem = loraInput.modemPreset, - onItemSelected = { loraInput = loraInput.copy { modemPreset = it } }, + selectedItem = formState.value.modemPreset, + onItemSelected = { formState.value = formState.value.copy { modemPreset = it } }, ) } item { HorizontalDivider() } @@ -124,30 +106,30 @@ fun LoRaConfigItemList( item { EditTextPreference( title = stringResource(R.string.bandwidth), - value = loraInput.bandwidth, - enabled = enabled && !loraInput.usePreset, + value = formState.value.bandwidth, + enabled = state.connected && !formState.value.usePreset, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { bandwidth = it } }, + onValueChanged = { formState.value = formState.value.copy { bandwidth = it } }, ) } item { EditTextPreference( title = stringResource(R.string.spread_factor), - value = loraInput.spreadFactor, - enabled = enabled && !loraInput.usePreset, + value = formState.value.spreadFactor, + enabled = state.connected && !formState.value.usePreset, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } }, + onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } }, ) } item { EditTextPreference( title = stringResource(R.string.coding_rate), - value = loraInput.codingRate, - enabled = enabled && !loraInput.usePreset, + value = formState.value.codingRate, + enabled = state.connected && !formState.value.usePreset, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { codingRate = it } }, + onValueChanged = { formState.value = formState.value.copy { codingRate = it } }, ) } } @@ -156,18 +138,18 @@ fun LoRaConfigItemList( item { SwitchPreference( title = stringResource(R.string.ignore_mqtt), - checked = loraInput.ignoreMqtt, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } }, + checked = formState.value.ignoreMqtt, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } }, ) } item { HorizontalDivider() } item { SwitchPreference( title = stringResource(R.string.ok_to_mqtt), - checked = loraInput.configOkToMqtt, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } }, + checked = formState.value.configOkToMqtt, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } }, ) } item { HorizontalDivider() } @@ -175,9 +157,9 @@ fun LoRaConfigItemList( item { SwitchPreference( title = stringResource(R.string.tx_enabled), - checked = loraInput.txEnabled, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } }, + checked = formState.value.txEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } }, ) } item { HorizontalDivider() } @@ -185,10 +167,10 @@ fun LoRaConfigItemList( EditTextPreference( title = stringResource(R.string.hop_limit), summary = stringResource(id = R.string.config_lora_hop_limit_summary), - value = loraInput.hopLimit, - enabled = enabled, + value = formState.value.hopLimit, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { hopLimit = it } }, + onValueChanged = { formState.value = formState.value.copy { hopLimit = it } }, ) } item { HorizontalDivider() } @@ -198,13 +180,18 @@ fun LoRaConfigItemList( EditTextPreference( title = stringResource(R.string.frequency_slot), summary = stringResource(id = R.string.config_lora_frequency_slot_summary), - value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum, - enabled = enabled, + value = + if (isFocused || formState.value.channelNum != 0) { + formState.value.channelNum + } else { + primaryChannel.channelNum + }, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onFocusChanged = { isFocused = it.isFocused }, onValueChanged = { - if (it <= loraInput.numChannels) { // total num of LoRa channels - loraInput = loraInput.copy { channelNum = it } + if (it <= formState.value.numChannels) { // total num of LoRa channels + formState.value = formState.value.copy { channelNum = it } } }, ) @@ -213,9 +200,9 @@ fun LoRaConfigItemList( item { SwitchPreference( title = stringResource(R.string.sx126x_rx_boosted_gain), - checked = loraInput.sx126XRxBoostedGain, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } }, + checked = formState.value.sx126XRxBoostedGain, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } }, ) } item { HorizontalDivider() } @@ -224,63 +211,38 @@ fun LoRaConfigItemList( EditTextPreference( title = stringResource(R.string.override_frequency_mhz), value = - if (isFocused || loraInput.overrideFrequency != 0f) { - loraInput.overrideFrequency + if (isFocused || formState.value.overrideFrequency != 0f) { + formState.value.overrideFrequency } else { primaryChannel.radioFreq }, - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onFocusChanged = { isFocused = it.isFocused }, - onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } }, + onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } }, ) } item { HorizontalDivider() } item { SignedIntegerEditTextPreference( title = stringResource(R.string.tx_power_dbm), - value = loraInput.txPower, - enabled = enabled, + value = formState.value.txPower, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { txPower = it } }, + onValueChanged = { formState.value = formState.value.copy { txPower = it } }, ) } - if (hasPaFan) { + if (viewModel.hasPaFan) { item { SwitchPreference( title = stringResource(R.string.pa_fan_disabled), - checked = loraInput.paFanDisabled, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } }, + checked = formState.value.paFanDisabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } }, ) } item { HorizontalDivider() } } - - item { - PreferenceFooter( - enabled = enabled && loraInput != loraConfig, - onCancelClicked = { - focusManager.clearFocus() - loraInput = loraConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(loraInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun LoRaConfigPreview() { - LoRaConfigItemList( - loraConfig = Channel.default.loraConfig, - primarySettings = Channel.default.settings, - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt index 056d5cee7..a9eeef776 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/MQTTConfigItemList.kt @@ -19,83 +19,72 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditPasswordPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun MQTTConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() val destNum = destNode?.num + val mqttConfig = state.moduleConfig.mqtt + val formState = rememberConfigState(initialValue = mqttConfig) - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) + if (!formState.value.mapReportSettings.shouldReportLocation) { + val settings = + formState.value.mapReportSettings.copy { + this.shouldReportLocation = viewModel.shouldReportLocation(destNum) + } + formState.value = formState.value.copy { mapReportSettings = settings } } - MQTTConfigItemList( - mqttConfig = state.moduleConfig.mqtt, - enabled = state.connected, - shouldReportLocation = viewModel.shouldReportLocation(destNum), - onShouldReportLocationChanged = { shouldReportLocation -> - viewModel.setShouldReportLocation(destNum, shouldReportLocation) - }, - onSaveClicked = { mqttInput -> - val config = moduleConfig { mqtt = mqttInput } + val consentValid = + if (formState.value.mapReportingEnabled) { + formState.value.mapReportSettings.shouldReportLocation && + mqttConfig.mapReportSettings.publishIntervalSecs >= MIN_INTERVAL_SECS + } else { + true + } + val focusManager = LocalFocusManager.current + + RadioConfigScreenList( + title = stringResource(id = R.string.mqtt), + onBack = { navController.popBackStack() }, + configState = formState, + enabled = state.connected && consentValid, + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { mqtt = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun MQTTConfigItemList( - mqttConfig: MQTTConfig, - enabled: Boolean, - shouldReportLocation: Boolean, - onShouldReportLocationChanged: (shouldReportLocation: Boolean) -> Unit, - onSaveClicked: (MQTTConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) } - if (!mqttInput.mapReportSettings.shouldReportLocation) { - val settings = mqttInput.mapReportSettings.copy { this.shouldReportLocation = shouldReportLocation } - mqttInput = mqttInput.copy { mapReportSettings = settings } - } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.mqtt_config)) } item { SwitchPreference( title = stringResource(R.string.mqtt_enabled), - checked = mqttInput.enabled, - enabled = enabled, - onCheckedChange = { mqttInput = mqttInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -103,48 +92,48 @@ fun MQTTConfigItemList( item { EditTextPreference( title = stringResource(R.string.address), - value = mqttInput.address, + value = formState.value.address, maxSize = 63, // address max_size:64 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { mqttInput = mqttInput.copy { address = it } }, + onValueChanged = { formState.value = formState.value.copy { address = it } }, ) } item { EditTextPreference( title = stringResource(R.string.username), - value = mqttInput.username, + value = formState.value.username, maxSize = 63, // username max_size:64 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { mqttInput = mqttInput.copy { username = it } }, + onValueChanged = { formState.value = formState.value.copy { username = it } }, ) } item { EditPasswordPreference( title = stringResource(R.string.password), - value = mqttInput.password, + value = formState.value.password, maxSize = 63, // password max_size:64 - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { mqttInput = mqttInput.copy { password = it } }, + onValueChanged = { formState.value = formState.value.copy { password = it } }, ) } item { SwitchPreference( title = stringResource(R.string.encryption_enabled), - checked = mqttInput.encryptionEnabled, - enabled = enabled, - onCheckedChange = { mqttInput = mqttInput.copy { encryptionEnabled = it } }, + checked = formState.value.encryptionEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { encryptionEnabled = it } }, ) } item { HorizontalDivider() } @@ -152,22 +141,22 @@ fun MQTTConfigItemList( item { SwitchPreference( title = stringResource(R.string.json_output_enabled), - checked = mqttInput.jsonEnabled, - enabled = enabled, - onCheckedChange = { mqttInput = mqttInput.copy { jsonEnabled = it } }, + checked = formState.value.jsonEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { jsonEnabled = it } }, ) } item { HorizontalDivider() } item { val defaultAddress = stringResource(R.string.default_mqtt_address) - val isDefault = mqttInput.address.isEmpty() || mqttInput.address.contains(defaultAddress) - val enforceTls = isDefault && mqttInput.proxyToClientEnabled + val isDefault = formState.value.address.isEmpty() || formState.value.address.contains(defaultAddress) + val enforceTls = isDefault && formState.value.proxyToClientEnabled SwitchPreference( title = stringResource(R.string.tls_enabled), - checked = mqttInput.tlsEnabled || enforceTls, - enabled = enabled && !enforceTls, - onCheckedChange = { mqttInput = mqttInput.copy { tlsEnabled = it } }, + checked = formState.value.tlsEnabled || enforceTls, + enabled = state.connected && !enforceTls, + onCheckedChange = { formState.value = formState.value.copy { tlsEnabled = it } }, ) } item { HorizontalDivider() } @@ -175,23 +164,23 @@ fun MQTTConfigItemList( item { EditTextPreference( title = stringResource(R.string.root_topic), - value = mqttInput.root, + value = formState.value.root, maxSize = 31, // root max_size:32 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { mqttInput = mqttInput.copy { root = it } }, + onValueChanged = { formState.value = formState.value.copy { root = it } }, ) } item { SwitchPreference( title = stringResource(R.string.proxy_to_client_enabled), - checked = mqttInput.proxyToClientEnabled, - enabled = enabled, - onCheckedChange = { mqttInput = mqttInput.copy { proxyToClientEnabled = it } }, + checked = formState.value.proxyToClientEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { proxyToClientEnabled = it } }, ) } item { HorizontalDivider() } @@ -200,63 +189,30 @@ fun MQTTConfigItemList( item { MapReportingPreference( - mapReportingEnabled = mqttInput.mapReportingEnabled, - onMapReportingEnabledChanged = { mqttInput = mqttInput.copy { mapReportingEnabled = it } }, - shouldReportLocation = mqttInput.mapReportSettings.shouldReportLocation, + mapReportingEnabled = formState.value.mapReportingEnabled, + onMapReportingEnabledChanged = { formState.value = formState.value.copy { mapReportingEnabled = it } }, + shouldReportLocation = formState.value.mapReportSettings.shouldReportLocation, onShouldReportLocationChanged = { - onShouldReportLocationChanged(it) - val settings = mqttInput.mapReportSettings.copy { this.shouldReportLocation = it } - mqttInput = mqttInput.copy { mapReportSettings = settings } + viewModel.setShouldReportLocation(destNum, it) + val settings = formState.value.mapReportSettings.copy { this.shouldReportLocation = it } + formState.value = formState.value.copy { mapReportSettings = settings } }, - positionPrecision = mqttInput.mapReportSettings.positionPrecision, + positionPrecision = formState.value.mapReportSettings.positionPrecision, onPositionPrecisionChanged = { - val settings = mqttInput.mapReportSettings.copy { positionPrecision = it } - mqttInput = mqttInput.copy { mapReportSettings = settings } + val settings = formState.value.mapReportSettings.copy { positionPrecision = it } + formState.value = formState.value.copy { mapReportSettings = settings } }, - publishIntervalSecs = mqttInput.mapReportSettings.publishIntervalSecs, + publishIntervalSecs = formState.value.mapReportSettings.publishIntervalSecs, onPublishIntervalSecsChanged = { - val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it } - mqttInput = mqttInput.copy { mapReportSettings = settings } + val settings = formState.value.mapReportSettings.copy { publishIntervalSecs = it } + formState.value = formState.value.copy { mapReportSettings = settings } }, - enabled = enabled, + enabled = state.connected, focusManager = focusManager, ) } item { HorizontalDivider() } - - item { - val consentValid = - if (mqttInput.mapReportingEnabled) { - mqttInput.mapReportSettings.shouldReportLocation && - mqttConfig.mapReportSettings.publishIntervalSecs >= MIN_INTERVAL_SECS - } else { - true - } - PreferenceFooter( - enabled = enabled && mqttInput != mqttConfig && consentValid, - onCancelClicked = { - focusManager.clearFocus() - mqttInput = mqttConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(mqttInput) - }, - ) - } } } private const val MIN_INTERVAL_SECS = 3600 - -@Preview(showBackground = true) -@Composable -private fun MQTTConfigPreview() { - MQTTConfigItemList( - mqttConfig = MQTTConfig.getDefaultInstance(), - enabled = true, - shouldReportLocation = true, - onShouldReportLocationChanged = { _ -> }, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt index 997c710c1..82f82d51a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NeighborInfoConfigItemList.kt @@ -17,67 +17,50 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun NeighborInfoConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val neighborInfoConfig = state.moduleConfig.neighborInfo + val formState = rememberConfigState(initialValue = neighborInfoConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - NeighborInfoConfigItemList( - neighborInfoConfig = state.moduleConfig.neighborInfo, + RadioConfigScreenList( + title = stringResource(id = R.string.neighbor_info), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { neighborInfoInput -> - val config = moduleConfig { neighborInfo = neighborInfoInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { neighborInfo = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun NeighborInfoConfigItemList( - neighborInfoConfig: ModuleConfigProtos.ModuleConfig.NeighborInfoConfig, - enabled: Boolean, - onSaveClicked: (ModuleConfigProtos.ModuleConfig.NeighborInfoConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var neighborInfoInput by rememberSaveable { mutableStateOf(neighborInfoConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.neighbor_info_config)) } item { SwitchPreference( title = stringResource(R.string.neighbor_info_enabled), - checked = neighborInfoInput.enabled, - enabled = enabled, - onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -85,10 +68,10 @@ fun NeighborInfoConfigItemList( item { EditTextPreference( title = stringResource(R.string.update_interval_seconds), - value = neighborInfoInput.updateInterval, - enabled = enabled, + value = formState.value.updateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { neighborInfoInput = neighborInfoInput.copy { updateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { updateInterval = it } }, ) } @@ -96,35 +79,11 @@ fun NeighborInfoConfigItemList( SwitchPreference( title = stringResource(R.string.transmit_over_lora), summary = stringResource(id = R.string.config_device_transmitOverLora_summary), - checked = neighborInfoInput.transmitOverLora, - enabled = enabled, - onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it } }, + checked = formState.value.transmitOverLora, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { transmitOverLora = it } }, ) HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && neighborInfoInput != neighborInfoConfig, - onCancelClicked = { - focusManager.clearFocus() - neighborInfoInput = neighborInfoConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(neighborInfoInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun NeighborInfoConfigPreview() { - NeighborInfoConfigItemList( - neighborInfoConfig = ModuleConfigProtos.ModuleConfig.NeighborInfoConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt index 83ba34aec..579efb9b6 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt @@ -18,11 +18,9 @@ package com.geeksville.mesh.ui.settings.radio.components import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -38,10 +36,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy @@ -50,7 +48,6 @@ import com.geeksville.mesh.ui.common.components.EditIPv4Preference import com.geeksville.mesh.ui.common.components.EditPasswordPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SimpleAlertDialog import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel @@ -63,40 +60,10 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) = SimpleAlertDialog(title = R.string.error, text = R.string.wifi_qr_code_error, onDismiss = onDismiss) @Composable -fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - NetworkConfigItemList( - hasWifi = state.metadata?.hasWifi ?: true, - hasEthernet = state.metadata?.hasEthernet ?: true, - networkConfig = state.radioConfig.network, - enabled = state.connected, - onSaveClicked = { networkInput -> - val config = config { network = networkInput } - viewModel.setConfig(config) - }, - ) -} - -private fun extractWifiCredentials(qrCode: String) = - Regex("""WIFI:S:(.*?);.*?P:(.*?);""").find(qrCode)?.destructured?.let { (ssid, password) -> ssid to password } - ?: (null to null) - -@Suppress("LongMethod", "CyclomaticComplexMethod") -@Composable -fun NetworkConfigItemList( - hasWifi: Boolean, - hasEthernet: Boolean, - networkConfig: NetworkConfig, - enabled: Boolean, - onSaveClicked: (NetworkConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var networkInput by rememberSaveable { mutableStateOf(networkConfig) } + val networkConfig = state.radioConfig.network + val formState = rememberConfigState(initialValue = networkConfig) var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) } if (showScanErrorDialog) { @@ -108,8 +75,8 @@ fun NetworkConfigItemList( if (result.contents != null) { val (ssid, psk) = extractWifiCredentials(result.contents) if (ssid != null && psk != null) { - networkInput = - networkInput.copy { + formState.value = + formState.value.copy { wifiSsid = ssid wifiPsk = psk } @@ -129,17 +96,29 @@ fun NetworkConfigItemList( } barcodeLauncher.launch(zxingScan) } + val focusManager = LocalFocusManager.current - LazyColumn(modifier = Modifier.fillMaxSize()) { - if (hasWifi) { + RadioConfigScreenList( + title = stringResource(id = R.string.network), + onBack = { navController.popBackStack() }, + configState = formState, + enabled = state.connected, + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { network = it } + viewModel.setConfig(config) + }, + ) { + if (state.metadata?.hasWifi == true) { item { PreferenceCategory(text = stringResource(R.string.wifi_config)) } item { SwitchPreference( title = stringResource(R.string.wifi_enabled), summary = stringResource(id = R.string.config_network_wifi_enabled_summary), - checked = networkInput.wifiEnabled, - enabled = enabled && hasWifi, - onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } }, + checked = formState.value.wifiEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { wifiEnabled = it } }, ) HorizontalDivider() } @@ -147,25 +126,25 @@ fun NetworkConfigItemList( item { EditTextPreference( title = stringResource(R.string.ssid), - value = networkInput.wifiSsid, + value = formState.value.wifiSsid, maxSize = 32, // wifi_ssid max_size:33 - enabled = enabled && hasWifi, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } }, + onValueChanged = { formState.value = formState.value.copy { wifiSsid = it } }, ) } item { EditPasswordPreference( title = stringResource(R.string.password), - value = networkInput.wifiPsk, + value = formState.value.wifiPsk, maxSize = 64, // wifi_psk max_size:65 - enabled = enabled && hasWifi, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } }, + onValueChanged = { formState.value = formState.value.copy { wifiPsk = it } }, ) } @@ -173,37 +152,38 @@ fun NetworkConfigItemList( Button( onClick = { zxingScan() }, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp), - enabled = enabled && hasWifi, + enabled = state.connected, ) { Text(text = stringResource(R.string.wifi_qr_code_scan)) } } } - if (hasEthernet) { + if (state.metadata?.hasEthernet == true) { item { PreferenceCategory(text = stringResource(R.string.ethernet_config)) } item { SwitchPreference( title = stringResource(R.string.ethernet_enabled), summary = stringResource(id = R.string.config_network_eth_enabled_summary), - checked = networkInput.ethEnabled, - enabled = enabled && hasEthernet, - onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } }, + checked = formState.value.ethEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { ethEnabled = it } }, ) HorizontalDivider() } } - if (hasEthernet || hasWifi) { + if (state.metadata?.hasEthernet == true || state.metadata?.hasWifi == true) { item { PreferenceCategory(text = stringResource(R.string.udp_config)) } item { SwitchPreference( title = stringResource(R.string.udp_enabled), summary = stringResource(id = R.string.config_network_udp_enabled_summary), - checked = networkInput.enabledProtocols == 1, - enabled = enabled, + checked = formState.value.enabledProtocols == 1, + enabled = state.connected, onCheckedChange = { - networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 } + formState.value = + formState.value.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 } }, ) } @@ -215,41 +195,41 @@ fun NetworkConfigItemList( item { EditTextPreference( title = stringResource(R.string.ntp_server), - value = networkInput.ntpServer, + value = formState.value.ntpServer, maxSize = 32, // ntp_server max_size:33 - enabled = enabled, - isError = networkInput.ntpServer.isEmpty(), + enabled = state.connected, + isError = formState.value.ntpServer.isEmpty(), keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { ntpServer = it } }, + onValueChanged = { formState.value = formState.value.copy { ntpServer = it } }, ) } item { EditTextPreference( title = stringResource(R.string.rsyslog_server), - value = networkInput.rsyslogServer, + value = formState.value.rsyslogServer, maxSize = 32, // rsyslog_server max_size:33 - enabled = enabled, + enabled = state.connected, isError = false, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { rsyslogServer = it } }, + onValueChanged = { formState.value = formState.value.copy { rsyslogServer = it } }, ) } item { DropDownPreference( title = stringResource(R.string.ipv4_mode), - enabled = enabled, + enabled = state.connected, items = NetworkConfig.AddressMode.entries .filter { it != NetworkConfig.AddressMode.UNRECOGNIZED } .map { it to it.name }, - selectedItem = networkInput.addressMode, - onItemSelected = { networkInput = networkInput.copy { addressMode = it } }, + selectedItem = formState.value.addressMode, + onItemSelected = { formState.value = formState.value.copy { addressMode = it } }, ) HorizontalDivider() } @@ -257,12 +237,12 @@ fun NetworkConfigItemList( item { EditIPv4Preference( title = stringResource(R.string.ip), - value = networkInput.ipv4Config.ip, - enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC, + value = formState.value.ipv4Config.ip, + enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - val ipv4 = networkInput.ipv4Config.copy { ip = it } - networkInput = networkInput.copy { ipv4Config = ipv4 } + val ipv4 = formState.value.ipv4Config.copy { ip = it } + formState.value = formState.value.copy { ipv4Config = ipv4 } }, ) } @@ -270,12 +250,12 @@ fun NetworkConfigItemList( item { EditIPv4Preference( title = stringResource(R.string.gateway), - value = networkInput.ipv4Config.gateway, - enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC, + value = formState.value.ipv4Config.gateway, + enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - val ipv4 = networkInput.ipv4Config.copy { gateway = it } - networkInput = networkInput.copy { ipv4Config = ipv4 } + val ipv4 = formState.value.ipv4Config.copy { gateway = it } + formState.value = formState.value.copy { ipv4Config = ipv4 } }, ) } @@ -283,12 +263,12 @@ fun NetworkConfigItemList( item { EditIPv4Preference( title = stringResource(R.string.subnet), - value = networkInput.ipv4Config.subnet, - enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC, + value = formState.value.ipv4Config.subnet, + enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - val ipv4 = networkInput.ipv4Config.copy { subnet = it } - networkInput = networkInput.copy { ipv4Config = ipv4 } + val ipv4 = formState.value.ipv4Config.copy { subnet = it } + formState.value = formState.value.copy { ipv4Config = ipv4 } }, ) } @@ -296,47 +276,19 @@ fun NetworkConfigItemList( item { EditIPv4Preference( title = "DNS", - value = networkInput.ipv4Config.dns, - enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC, + value = formState.value.ipv4Config.dns, + enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { - val ipv4 = networkInput.ipv4Config.copy { dns = it } - networkInput = networkInput.copy { ipv4Config = ipv4 } + val ipv4 = formState.value.ipv4Config.copy { dns = it } + formState.value = formState.value.copy { ipv4Config = ipv4 } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && networkInput != networkConfig, - onCancelClicked = { - focusManager.clearFocus() - networkInput = networkConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(networkInput) - }, - ) - } } } -@Preview(showBackground = true) -@Composable -private fun NetworkConfigPreview() { - NetworkConfigItemList( - hasWifi = true, - hasEthernet = true, - networkConfig = NetworkConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} - -@Preview(showBackground = true) -@Composable -private fun QrCodeErrorDialogPreview() { - ScanErrorDialog() -} +private fun extractWifiCredentials(qrCode: String) = + Regex("""WIFI:S:(.*?);.*?P:(.*?);""").find(qrCode)?.destructured?.let { (ssid, password) -> ssid to password } + ?: (null to null) diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt index 3b0818840..458bde52c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PaxcounterConfigItemList.kt @@ -17,69 +17,51 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun PaxcounterConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val paxcounterConfig = state.moduleConfig.paxcounter + val formState = rememberConfigState(initialValue = paxcounterConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - PaxcounterConfigItemList( - paxcounterConfig = state.moduleConfig.paxcounter, + RadioConfigScreenList( + title = stringResource(id = R.string.paxcounter), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { paxcounterConfigInput -> - val config = moduleConfig { paxcounter = paxcounterConfigInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { paxcounter = it } viewModel.setModuleConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun PaxcounterConfigItemList( - paxcounterConfig: ModuleConfigProtos.ModuleConfig.PaxcounterConfig, - enabled: Boolean, - onSaveClicked: (ModuleConfigProtos.ModuleConfig.PaxcounterConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var paxcounterInput by rememberSaveable { mutableStateOf(paxcounterConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.paxcounter_config)) } item { SwitchPreference( title = stringResource(R.string.paxcounter_enabled), - checked = paxcounterInput.enabled, - enabled = enabled, - onCheckedChange = { paxcounterInput = paxcounterInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -87,55 +69,31 @@ fun PaxcounterConfigItemList( item { EditTextPreference( title = stringResource(R.string.update_interval_seconds), - value = paxcounterInput.paxcounterUpdateInterval, - enabled = enabled, + value = formState.value.paxcounterUpdateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { paxcounterUpdateInterval = it } }, ) } item { SignedIntegerEditTextPreference( title = stringResource(R.string.wifi_rssi_threshold_defaults_to_80), - value = paxcounterInput.wifiThreshold, - enabled = enabled, + value = formState.value.wifiThreshold, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { paxcounterInput = paxcounterInput.copy { wifiThreshold = it } }, + onValueChanged = { formState.value = formState.value.copy { wifiThreshold = it } }, ) } item { SignedIntegerEditTextPreference( title = stringResource(R.string.ble_rssi_threshold_defaults_to_80), - value = paxcounterInput.bleThreshold, - enabled = enabled, + value = formState.value.bleThreshold, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { paxcounterInput = paxcounterInput.copy { bleThreshold = it } }, - ) - } - - item { - PreferenceFooter( - enabled = enabled && paxcounterInput != paxcounterConfig, - onCancelClicked = { - focusManager.clearFocus() - paxcounterInput = paxcounterConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(paxcounterInput) - }, + onValueChanged = { formState.value = formState.value.copy { bleThreshold = it } }, ) } } } - -@Preview(showBackground = true) -@Composable -private fun PaxcounterConfigPreview() { - PaxcounterConfigItemList( - paxcounterConfig = ModuleConfigProtos.ModuleConfig.PaxcounterConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt index c174a9bd6..ba5636263 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt @@ -21,8 +21,6 @@ import android.Manifest import android.annotation.SuppressLint import android.location.Location import android.os.Build -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -35,13 +33,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.core.location.LocationCompat import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos import com.geeksville.mesh.ConfigProtos.Config.PositionConfig import com.geeksville.mesh.Position @@ -51,7 +48,6 @@ import com.geeksville.mesh.ui.common.components.BitwisePreference import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -61,7 +57,7 @@ import org.meshtastic.core.strings.R @OptIn(ExperimentalPermissionsApi::class) @Composable -fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() var phoneLocation: Location? by remember { mutableStateOf(null) } @@ -73,120 +69,104 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { altitude = node?.position?.altitude ?: 0, time = 1, // ignore time for fixed_position ) + val positionConfig = state.radioConfig.position + val formState = rememberConfigState(initialValue = positionConfig) + var locationInput by rememberSaveable { mutableStateOf(currentPosition) } - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - PositionConfigItemList( - phoneLocation = phoneLocation, - location = currentPosition, - positionConfig = state.radioConfig.position, - enabled = state.connected, - onSaveClicked = { locationInput, positionInput -> - if (positionInput.fixedPosition) { - if (locationInput != currentPosition) { - viewModel.setFixedPosition(locationInput) - } - } else { - if (state.radioConfig.position.fixedPosition) { - // fixed position changed from enabled to disabled - viewModel.removeFixedPosition() - } - } - val config = config { position = positionInput } - viewModel.setConfig(config) - }, - onUseCurrentLocation = { - @SuppressLint("MissingPermission") - coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() } - }, - ) -} - -@OptIn(ExperimentalPermissionsApi::class) -@Suppress("LongMethod", "CyclomaticComplexMethod") -@Composable -fun PositionConfigItemList( - phoneLocation: Location? = null, - location: Position, - positionConfig: PositionConfig, - enabled: Boolean, - onSaveClicked: (position: Position, config: PositionConfig) -> Unit, - onUseCurrentLocation: suspend () -> Unit, -) { - val focusManager = LocalFocusManager.current - val coroutineScope = rememberCoroutineScope() val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) { granted -> if (granted) { - coroutineScope.launch { onUseCurrentLocation() } + @SuppressLint("MissingPermission") + coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() } } } - var locationInput by rememberSaveable { mutableStateOf(location) } - var positionInput by rememberSaveable { mutableStateOf(positionConfig) } LaunchedEffect(phoneLocation) { - if (phoneLocation != null) { + phoneLocation?.let { phoneLoc -> locationInput = Position( - latitude = phoneLocation.latitude, - longitude = phoneLocation.longitude, + latitude = phoneLoc.latitude, + longitude = phoneLoc.longitude, altitude = - LocationCompat.hasMslAltitude(phoneLocation).let { + LocationCompat.hasMslAltitude(phoneLoc).let { if (it && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - phoneLocation.mslAltitudeMeters.toInt() + phoneLoc.mslAltitudeMeters.toInt() } else { - phoneLocation.altitude.toInt() + phoneLoc.altitude.toInt() } }, ) } } - LazyColumn(modifier = Modifier.fillMaxSize()) { + val focusManager = LocalFocusManager.current + + RadioConfigScreenList( + title = stringResource(id = R.string.position), + onBack = { navController.popBackStack() }, + configState = formState, + enabled = state.connected, + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + if (formState.value.fixedPosition) { + if (locationInput != currentPosition) { + viewModel.setFixedPosition(locationInput) + } + } else { + if (positionConfig.fixedPosition) { + // fixed position changed from enabled to disabled + viewModel.removeFixedPosition() + } + } + val config = config { position = it } + viewModel.setConfig(config) + }, + ) { item { PreferenceCategory(text = stringResource(R.string.position_packet)) } item { EditTextPreference( title = stringResource(R.string.broadcast_interval), summary = stringResource(id = R.string.config_position_broadcast_secs_summary), - value = positionInput.positionBroadcastSecs, - enabled = enabled, + value = formState.value.positionBroadcastSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { positionBroadcastSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { positionBroadcastSecs = it } }, ) } item { SwitchPreference( title = stringResource(R.string.smart_position), - checked = positionInput.positionBroadcastSmartEnabled, - enabled = enabled, - onCheckedChange = { positionInput = positionInput.copy { positionBroadcastSmartEnabled = it } }, + checked = formState.value.positionBroadcastSmartEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { positionBroadcastSmartEnabled = it } }, ) } item { HorizontalDivider() } - if (positionInput.positionBroadcastSmartEnabled) { + if (formState.value.positionBroadcastSmartEnabled) { item { EditTextPreference( title = stringResource(R.string.minimum_interval), summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_interval_secs_summary), - value = positionInput.broadcastSmartMinimumIntervalSecs, - enabled = enabled, + value = formState.value.broadcastSmartMinimumIntervalSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it } }, + onValueChanged = { + formState.value = formState.value.copy { broadcastSmartMinimumIntervalSecs = it } + }, ) } item { EditTextPreference( title = stringResource(R.string.minimum_distance), summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary), - value = positionInput.broadcastSmartMinimumDistance, - enabled = enabled, + value = formState.value.broadcastSmartMinimumDistance, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } }, + onValueChanged = { formState.value = formState.value.copy { broadcastSmartMinimumDistance = it } }, ) } } @@ -194,19 +174,19 @@ fun PositionConfigItemList( item { SwitchPreference( title = stringResource(R.string.fixed_position), - checked = positionInput.fixedPosition, - enabled = enabled, - onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } }, + checked = formState.value.fixedPosition, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { fixedPosition = it } }, ) } item { HorizontalDivider() } - if (positionInput.fixedPosition) { + if (formState.value.fixedPosition) { item { EditTextPreference( title = stringResource(R.string.latitude), value = locationInput.latitude, - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { value -> if (value >= -90 && value <= 90.0) { @@ -219,7 +199,7 @@ fun PositionConfigItemList( EditTextPreference( title = stringResource(R.string.longitude), value = locationInput.longitude, - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { value -> if (value >= -180 && value <= 180.0) { @@ -232,14 +212,14 @@ fun PositionConfigItemList( EditTextPreference( title = stringResource(R.string.altitude), value = locationInput.altitude, - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) }, ) } item { TextButton( - enabled = enabled, + enabled = state.connected, onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } }, ) { Text(text = stringResource(R.string.position_config_set_fixed_from_phone)) @@ -250,13 +230,13 @@ fun PositionConfigItemList( item { DropDownPreference( title = stringResource(R.string.gps_mode), - enabled = enabled, + enabled = state.connected, items = ConfigProtos.Config.PositionConfig.GpsMode.entries .filter { it != ConfigProtos.Config.PositionConfig.GpsMode.UNRECOGNIZED } .map { it to it.name }, - selectedItem = positionInput.gpsMode, - onItemSelected = { positionInput = positionInput.copy { gpsMode = it } }, + selectedItem = formState.value.gpsMode, + onItemSelected = { formState.value = formState.value.copy { gpsMode = it } }, ) } item { HorizontalDivider() } @@ -265,10 +245,10 @@ fun PositionConfigItemList( EditTextPreference( title = stringResource(R.string.update_interval), summary = stringResource(id = R.string.config_position_gps_update_interval_summary), - value = positionInput.gpsUpdateInterval, - enabled = enabled, + value = formState.value.gpsUpdateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { gpsUpdateInterval = it } }, ) } item { PreferenceCategory(text = stringResource(R.string.position_flags)) } @@ -276,15 +256,15 @@ fun PositionConfigItemList( BitwisePreference( title = stringResource(R.string.position_flags), summary = stringResource(id = R.string.config_position_flags_summary), - value = positionInput.positionFlags, - enabled = enabled, + value = formState.value.positionFlags, + enabled = state.connected, items = ConfigProtos.Config.PositionConfig.PositionFlags.entries .filter { it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED } .map { it.number to it.name }, - onItemSelected = { positionInput = positionInput.copy { positionFlags = it } }, + onItemSelected = { formState.value = formState.value.copy { positionFlags = it } }, ) } item { HorizontalDivider() } @@ -293,58 +273,31 @@ fun PositionConfigItemList( item { EditTextPreference( title = stringResource(R.string.gps_receive_gpio), - value = positionInput.rxGpio, - enabled = enabled, + value = formState.value.rxGpio, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { rxGpio = it } }, + onValueChanged = { formState.value = formState.value.copy { rxGpio = it } }, ) } item { EditTextPreference( title = stringResource(R.string.gps_transmit_gpio), - value = positionInput.txGpio, - enabled = enabled, + value = formState.value.txGpio, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { txGpio = it } }, + onValueChanged = { formState.value = formState.value.copy { txGpio = it } }, ) } item { EditTextPreference( title = stringResource(R.string.gps_en_gpio), - value = positionInput.gpsEnGpio, - enabled = enabled, + value = formState.value.gpsEnGpio, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { gpsEnGpio = it } }, - ) - } - - item { - PreferenceFooter( - enabled = enabled && positionInput != positionConfig || locationInput != location, - onCancelClicked = { - focusManager.clearFocus() - locationInput = location - positionInput = positionConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(locationInput, positionInput) - }, + onValueChanged = { formState.value = formState.value.copy { gpsEnGpio = it } }, ) } } } - -@Preview(showBackground = true) -@Composable -private fun PositionConfigPreview() { - PositionConfigItemList( - location = Position(0.0, 0.0, 0), - positionConfig = PositionConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = { _, _ -> }, - onUseCurrentLocation = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt index 1e93f63d4..5f538beef 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt @@ -17,67 +17,51 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ConfigProtos.Config.PowerConfig +import androidx.navigation.NavController import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun PowerConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val powerConfig = state.radioConfig.power + val formState = rememberConfigState(initialValue = powerConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - PowerConfigItemList( - powerConfig = state.radioConfig.power, + RadioConfigScreenList( + title = stringResource(id = R.string.power), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { powerInput -> - val config = config { power = powerInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { power = it } viewModel.setConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit) { - val focusManager = LocalFocusManager.current - var powerInput by rememberSaveable { mutableStateOf(powerConfig) } - var shutdownOnPowerLoss by rememberSaveable { mutableStateOf(powerConfig.onBatteryShutdownAfterSecs > 0) } - var adcOverride by rememberSaveable { mutableStateOf(powerConfig.adcMultiplierOverride > 0f) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.power_config)) } item { SwitchPreference( title = stringResource(R.string.enable_power_saving_mode), summary = stringResource(id = R.string.config_power_is_power_saving_summary), - checked = powerInput.isPowerSaving, - enabled = enabled, - onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } }, + checked = formState.value.isPowerSaving, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { isPowerSaving = it } }, ) } item { HorizontalDivider() } @@ -85,23 +69,22 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke item { SwitchPreference( title = stringResource(R.string.shutdown_on_power_loss), - checked = shutdownOnPowerLoss, - enabled = enabled, + checked = formState.value.onBatteryShutdownAfterSecs > 0, + enabled = state.connected, onCheckedChange = { - shutdownOnPowerLoss = it - if (!it) powerInput = powerInput.copy { onBatteryShutdownAfterSecs = 0 } + formState.value = formState.value.copy { onBatteryShutdownAfterSecs = if (it) 3600 else 0 } }, ) } - if (shutdownOnPowerLoss) { + if (formState.value.onBatteryShutdownAfterSecs > 0) { item { EditTextPreference( title = stringResource(R.string.shutdown_on_battery_delay_seconds), - value = powerInput.onBatteryShutdownAfterSecs, - enabled = enabled, + value = formState.value.onBatteryShutdownAfterSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { onBatteryShutdownAfterSecs = it } }, ) } } @@ -111,23 +94,22 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke item { SwitchPreference( title = stringResource(R.string.adc_multiplier_override), - checked = adcOverride, - enabled = enabled, + checked = formState.value.adcMultiplierOverride > 0f, + enabled = state.connected, onCheckedChange = { - adcOverride = it - if (!it) powerInput = powerInput.copy { adcMultiplierOverride = 0f } + formState.value = formState.value.copy { adcMultiplierOverride = if (it) 1.0f else 0.0f } }, ) } - if (adcOverride) { + if (formState.value.adcMultiplierOverride > 0f) { item { EditTextPreference( title = stringResource(R.string.adc_multiplier_override_ratio), - value = powerInput.adcMultiplierOverride, - enabled = enabled, + value = formState.value.adcMultiplierOverride, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } }, + onValueChanged = { formState.value = formState.value.copy { adcMultiplierOverride = it } }, ) } } @@ -137,61 +119,41 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke item { EditTextPreference( title = stringResource(R.string.wait_for_bluetooth_duration_seconds), - value = powerInput.waitBluetoothSecs, - enabled = enabled, + value = formState.value.waitBluetoothSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { waitBluetoothSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { waitBluetoothSecs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.super_deep_sleep_duration_seconds), - value = powerInput.sdsSecs, - enabled = enabled, + value = formState.value.sdsSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { sdsSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { sdsSecs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.minimum_wake_time_seconds), - value = powerInput.minWakeSecs, - enabled = enabled, + value = formState.value.minWakeSecs, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { minWakeSecs = it } }, + onValueChanged = { formState.value = formState.value.copy { minWakeSecs = it } }, ) } item { EditTextPreference( title = stringResource(R.string.battery_ina_2xx_i2c_address), - value = powerInput.deviceBatteryInaAddress, - enabled = enabled, + value = formState.value.deviceBatteryInaAddress, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { deviceBatteryInaAddress = it } }, - ) - } - - item { - PreferenceFooter( - enabled = enabled && powerInput != powerConfig, - onCancelClicked = { - focusManager.clearFocus() - powerInput = powerConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(powerInput) - }, + onValueChanged = { formState.value = formState.value.copy { deviceBatteryInaAddress = it } }, ) } } } - -@Preview(showBackground = true) -@Composable -private fun PowerConfigPreview() { - PowerConfigItemList(powerConfig = PowerConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt new file mode 100644 index 000000000..e70c2ff9d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.settings.radio.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import com.geeksville.mesh.ui.common.components.MainAppBar +import com.geeksville.mesh.ui.common.components.PreferenceFooter +import com.geeksville.mesh.ui.settings.radio.ResponseState +import com.google.protobuf.MessageLite + +@Composable +fun RadioConfigScreenList( + title: String, + onBack: () -> Unit, + responseState: ResponseState, + onDismissPacketResponse: () -> Unit, + configState: ConfigState, + enabled: Boolean, + onSave: (T) -> Unit, + content: LazyListScope.() -> Unit, +) { + val focusManager = LocalFocusManager.current + + if (responseState.isWaiting()) { + PacketResponseStateDialog(state = responseState, onDismiss = onDismissPacketResponse) + } + + Scaffold( + topBar = { + MainAppBar( + title = title, + canNavigateUp = true, + onNavigateUp = onBack, + ourNode = null, + isConnected = false, + showNodeChip = false, + actions = {}, + onAction = {}, + ) + }, + ) { innerPadding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) { + content() + item { + PreferenceFooter( + enabled = enabled && configState.isDirty, + onCancelClicked = { + focusManager.clearFocus() + configState.reset() + }, + onSaveClicked = { + focusManager.clearFocus() + onSave(configState.value) + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt index e5145fdae..0ba8567bd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RangeTestConfigItemList.kt @@ -17,67 +17,50 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun RangeTestConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val rangeTestConfig = state.moduleConfig.rangeTest + val formState = rememberConfigState(initialValue = rangeTestConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - RangeTestConfigItemList( - rangeTestConfig = state.moduleConfig.rangeTest, + RadioConfigScreenList( + title = stringResource(id = R.string.range_test), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { rangeTestInput -> - val config = moduleConfig { rangeTest = rangeTestInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { rangeTest = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun RangeTestConfigItemList( - rangeTestConfig: RangeTestConfig, - enabled: Boolean, - onSaveClicked: (RangeTestConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var rangeTestInput by rememberSaveable { mutableStateOf(rangeTestConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.range_test_config)) } item { SwitchPreference( title = stringResource(R.string.range_test_enabled), - checked = rangeTestInput.enabled, - enabled = enabled, - onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -85,41 +68,21 @@ fun RangeTestConfigItemList( item { EditTextPreference( title = stringResource(R.string.sender_message_interval_seconds), - value = rangeTestInput.sender, - enabled = enabled, + value = formState.value.sender, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } }, + onValueChanged = { formState.value = formState.value.copy { sender = it } }, ) } item { SwitchPreference( title = stringResource(R.string.save_csv_in_storage_esp32_only), - checked = rangeTestInput.save, - enabled = enabled, - onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } }, + checked = formState.value.save, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { save = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && rangeTestInput != rangeTestConfig, - onCancelClicked = { - focusManager.clearFocus() - rangeTestInput = rangeTestConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(rangeTestInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun RangeTestConfig() { - RangeTestConfigItemList(rangeTestConfig = RangeTestConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt index 1e438a44f..d641a073d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RemoteHardwareConfigItemList.kt @@ -17,67 +17,50 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditListPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun RemoteHardwareConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val remoteHardwareConfig = state.moduleConfig.remoteHardware + val formState = rememberConfigState(initialValue = remoteHardwareConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - RemoteHardwareConfigItemList( - remoteHardwareConfig = state.moduleConfig.remoteHardware, + RadioConfigScreenList( + title = stringResource(id = R.string.remote_hardware), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { remoteHardwareInput -> - val config = moduleConfig { remoteHardware = remoteHardwareInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { remoteHardware = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun RemoteHardwareConfigItemList( - remoteHardwareConfig: RemoteHardwareConfig, - enabled: Boolean, - onSaveClicked: (RemoteHardwareConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var remoteHardwareInput by rememberSaveable { mutableStateOf(remoteHardwareConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.remote_hardware_config)) } item { SwitchPreference( title = stringResource(R.string.remote_hardware_enabled), - checked = remoteHardwareInput.enabled, - enabled = enabled, - onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -85,9 +68,9 @@ fun RemoteHardwareConfigItemList( item { SwitchPreference( title = stringResource(R.string.allow_undefined_pin_access), - checked = remoteHardwareInput.allowUndefinedPinAccess, - enabled = enabled, - onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it } }, + checked = formState.value.allowUndefinedPinAccess, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { allowUndefinedPinAccess = it } }, ) } item { HorizontalDivider() } @@ -95,42 +78,18 @@ fun RemoteHardwareConfigItemList( item { EditListPreference( title = stringResource(R.string.available_pins), - list = remoteHardwareInput.availablePinsList, + list = formState.value.availablePinsList, maxCount = 4, // available_pins max_count:4 - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValuesChanged = { list -> - remoteHardwareInput = - remoteHardwareInput.copy { + formState.value = + formState.value.copy { availablePins.clear() availablePins.addAll(list) } }, ) } - - item { - PreferenceFooter( - enabled = enabled && remoteHardwareInput != remoteHardwareConfig, - onCancelClicked = { - focusManager.clearFocus() - remoteHardwareInput = remoteHardwareConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(remoteHardwareInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun RemoteHardwareConfigPreview() { - RemoteHardwareConfigItemList( - remoteHardwareConfig = RemoteHardwareConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt index cd6ebab5d..41b241c49 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt @@ -19,12 +19,9 @@ package com.geeksville.mesh.ui.settings.radio.components import android.app.Activity import android.content.Intent -import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Warning @@ -42,19 +39,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig -import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.config import com.geeksville.mesh.copy import com.geeksville.mesh.ui.common.components.CopyIconButton import com.geeksville.mesh.ui.common.components.EditBase64Preference import com.geeksville.mesh.ui.common.components.EditListPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.node.NodeActionButton import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel @@ -64,45 +59,19 @@ import com.google.protobuf.ByteString import org.meshtastic.core.strings.R import java.security.SecureRandom +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun SecurityConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val node by viewModel.destNode.collectAsStateWithLifecycle() + val securityConfig = state.radioConfig.security + val formState = rememberConfigState(initialValue = securityConfig) - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - SecurityConfigItemList( - user = node?.user, - securityConfig = state.radioConfig.security, - enabled = state.connected, - onConfirm = { securityInput -> - val config = config { security = securityInput } - viewModel.setConfig(config) - }, - onExport = { uri, securityConfig -> viewModel.exportSecurityConfig(uri, securityConfig) }, - ) -} - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Suppress("LongMethod") -@Composable -fun SecurityConfigItemList( - user: MeshProtos.User? = null, - securityConfig: SecurityConfig, - enabled: Boolean, - onConfirm: (config: SecurityConfig) -> Unit, - onExport: (uri: Uri, securityConfig: SecurityConfig) -> Unit = { _, _ -> }, -) { - val focusManager = LocalFocusManager.current - var securityInput by rememberSaveable { mutableStateOf(securityConfig) } - - var publicKey by rememberSaveable { mutableStateOf(securityInput.publicKey) } - LaunchedEffect(securityInput.privateKey) { - if (securityInput.privateKey != securityConfig.privateKey) { + var publicKey by rememberSaveable { mutableStateOf(formState.value.publicKey) } + LaunchedEffect(formState.value.privateKey) { + if (formState.value.privateKey != securityConfig.privateKey) { publicKey = "".toByteString() - } else if (securityInput.privateKey == securityConfig.privateKey) { + } else if (formState.value.privateKey == securityConfig.privateKey) { publicKey = securityConfig.publicKey } } @@ -110,18 +79,18 @@ fun SecurityConfigItemList( val exportConfigLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { uri -> onExport(uri, securityConfig) } + it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri, securityConfig) } } } var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) } PrivateKeyRegenerateDialog( showKeyGenerationDialog = showKeyGenerationDialog, - config = securityInput, - onConfirm = { newConfig -> - securityInput = newConfig + onConfirm = { + formState.value = it showKeyGenerationDialog = false - onConfirm(securityInput) + val config = config { security = formState.value } + viewModel.setConfig(config) }, onDismiss = { showKeyGenerationDialog = false }, ) @@ -141,7 +110,7 @@ fun SecurityConfigItemList( type = "application/*" putExtra( Intent.EXTRA_TITLE, - "${user?.shortName}_keys_${System.currentTimeMillis()}.json", + "${node?.user?.shortName}_keys_${System.currentTimeMillis()}.json", ) } exportConfigLauncher.launch(intent) @@ -153,7 +122,19 @@ fun SecurityConfigItemList( ) } - LazyColumn(modifier = Modifier.fillMaxSize()) { + val focusManager = LocalFocusManager.current + RadioConfigScreenList( + title = stringResource(id = R.string.security), + onBack = { navController.popBackStack() }, + configState = formState, + enabled = state.connected, + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = config { security = it } + viewModel.setConfig(config) + }, + ) { item { PreferenceCategory(text = stringResource(R.string.direct_message_key)) } item { @@ -161,15 +142,15 @@ fun SecurityConfigItemList( title = stringResource(R.string.public_key), summary = stringResource(id = R.string.config_security_public_key), value = publicKey, - enabled = enabled, + enabled = state.connected, readOnly = true, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size() == 32) { - securityInput = securityInput.copy { this.publicKey = it } + formState.value = formState.value.copy { this.publicKey = it } } }, - trailingIcon = { CopyIconButton(valueToCopy = securityInput.publicKey.encodeToString()) }, + trailingIcon = { CopyIconButton(valueToCopy = formState.value.publicKey.encodeToString()) }, ) } @@ -177,15 +158,15 @@ fun SecurityConfigItemList( EditBase64Preference( title = stringResource(R.string.private_key), summary = stringResource(id = R.string.config_security_private_key), - value = securityInput.privateKey, - enabled = enabled, + value = formState.value.privateKey, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size() == 32) { - securityInput = securityInput.copy { privateKey = it } + formState.value = formState.value.copy { privateKey = it } } }, - trailingIcon = { CopyIconButton(valueToCopy = securityInput.privateKey.encodeToString()) }, + trailingIcon = { CopyIconButton(valueToCopy = formState.value.privateKey.encodeToString()) }, ) } @@ -193,7 +174,7 @@ fun SecurityConfigItemList( NodeActionButton( modifier = Modifier.padding(horizontal = 8.dp), title = stringResource(R.string.regenerate_private_key), - enabled = enabled, + enabled = state.connected, icon = Icons.TwoTone.Warning, onClick = { showKeyGenerationDialog = true }, ) @@ -203,7 +184,7 @@ fun SecurityConfigItemList( NodeActionButton( modifier = Modifier.padding(horizontal = 8.dp), title = stringResource(R.string.export_keys), - enabled = enabled, + enabled = state.connected, icon = Icons.TwoTone.Warning, onClick = { showEditSecurityConfigDialog = true }, ) @@ -213,13 +194,13 @@ fun SecurityConfigItemList( EditListPreference( title = stringResource(R.string.admin_key), summary = stringResource(id = R.string.config_security_admin_key), - list = securityInput.adminKeyList, + list = formState.value.adminKeyList, maxCount = 3, - enabled = enabled, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValuesChanged = { - securityInput = - securityInput.copy { + formState.value = + formState.value.copy { adminKey.clear() adminKey.addAll(it) } @@ -231,9 +212,9 @@ fun SecurityConfigItemList( SwitchPreference( title = stringResource(R.string.serial_console), summary = stringResource(id = R.string.config_security_serial_enabled), - checked = securityInput.serialEnabled, - enabled = enabled, - onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } }, + checked = formState.value.serialEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { serialEnabled = it } }, ) } item { HorizontalDivider() } @@ -242,9 +223,9 @@ fun SecurityConfigItemList( SwitchPreference( title = stringResource(R.string.debug_log_api_enabled), summary = stringResource(id = R.string.config_security_debug_log_api_enabled), - checked = securityInput.debugLogApiEnabled, - enabled = enabled, - onCheckedChange = { securityInput = securityInput.copy { debugLogApiEnabled = it } }, + checked = formState.value.debugLogApiEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { debugLogApiEnabled = it } }, ) } item { HorizontalDivider() } @@ -253,9 +234,9 @@ fun SecurityConfigItemList( SwitchPreference( title = stringResource(R.string.managed_mode), summary = stringResource(id = R.string.config_security_is_managed), - checked = securityInput.isManaged, - enabled = enabled && securityInput.adminKeyCount > 0, - onCheckedChange = { securityInput = securityInput.copy { isManaged = it } }, + checked = formState.value.isManaged, + enabled = state.connected && formState.value.adminKeyCount > 0, + onCheckedChange = { formState.value = formState.value.copy { isManaged = it } }, ) } item { HorizontalDivider() } @@ -263,26 +244,12 @@ fun SecurityConfigItemList( item { SwitchPreference( title = stringResource(R.string.legacy_admin_channel), - checked = securityInput.adminChannelEnabled, - enabled = enabled, - onCheckedChange = { securityInput = securityInput.copy { adminChannelEnabled = it } }, + checked = formState.value.adminChannelEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { adminChannelEnabled = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && securityInput != securityConfig, - onCancelClicked = { - focusManager.clearFocus() - securityInput = securityConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onConfirm(securityInput) - }, - ) - } } } @@ -290,11 +257,9 @@ fun SecurityConfigItemList( @Composable fun PrivateKeyRegenerateDialog( showKeyGenerationDialog: Boolean, - config: SecurityConfig, onConfirm: (SecurityConfig) -> Unit, onDismiss: () -> Unit = {}, ) { - var securityInput by rememberSaveable { mutableStateOf(config) } if (showKeyGenerationDialog) { AlertDialog( onDismissRequest = onDismiss, @@ -303,20 +268,22 @@ fun PrivateKeyRegenerateDialog( confirmButton = { TextButton( onClick = { - securityInput = - securityInput.copy { - clearPrivateKey() - clearPublicKey() - // Generate a random "f" value - val f = ByteArray(32).apply { SecureRandom().nextBytes(this) } - // Adjust the value to make it valid as an "s" value for eval(). - // According to the specification we need to mask off the 3 - // right-most bits of f[0], mask off the left-most bit of f[31], - // and set the second to left-most bit of f[31]. - f[0] = (f[0].toInt() and 0xF8).toByte() - f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte() - privateKey = ByteString.copyFrom(f) - } + val securityInput = + SecurityConfig.newBuilder() + .apply { + clearPrivateKey() + clearPublicKey() + // Generate a random "f" value + val f = ByteArray(32).apply { SecureRandom().nextBytes(this) } + // Adjust the value to make it valid as an "s" value for eval(). + // According to the specification we need to mask off the 3 + // right-most bits of f[0], mask off the left-most bit of f[31], + // and set the second to left-most bit of f[31]. + f[0] = (f[0].toInt() and 0xF8).toByte() + f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte() + privateKey = ByteString.copyFrom(f) + } + .build() onConfirm(securityInput) }, ) { @@ -327,9 +294,3 @@ fun PrivateKeyRegenerateDialog( ) } } - -@Preview(showBackground = true) -@Composable -private fun SecurityConfigPreview() { - SecurityConfigItemList(securityConfig = SecurityConfig.getDefaultInstance(), enabled = true, onConfirm = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt index 1be9e9a36..54c619cfd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SerialConfigItemList.kt @@ -17,65 +17,52 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.DropDownPreference import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun SerialConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val serialConfig = state.moduleConfig.serial + val formState = rememberConfigState(initialValue = serialConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - SerialConfigItemList( - serialConfig = state.moduleConfig.serial, + RadioConfigScreenList( + title = stringResource(id = R.string.serial), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { serialInput -> - val config = moduleConfig { serial = serialInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { serial = it } viewModel.setModuleConfig(config) }, - ) -} - -@Suppress("LongMethod") -@Composable -fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit) { - val focusManager = LocalFocusManager.current - var serialInput by rememberSaveable { mutableStateOf(serialConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.serial_config)) } item { SwitchPreference( title = stringResource(R.string.serial_enabled), - checked = serialInput.enabled, - enabled = enabled, - onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -83,9 +70,9 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli item { SwitchPreference( title = stringResource(R.string.echo_enabled), - checked = serialInput.echo, - enabled = enabled, - onCheckedChange = { serialInput = serialInput.copy { echo = it } }, + checked = formState.value.echo, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { echo = it } }, ) } item { HorizontalDivider() } @@ -93,33 +80,33 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli item { EditTextPreference( title = "RX", - value = serialInput.rxd, - enabled = enabled, + value = formState.value.rxd, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { serialInput = serialInput.copy { rxd = it } }, + onValueChanged = { formState.value = formState.value.copy { rxd = it } }, ) } item { EditTextPreference( title = "TX", - value = serialInput.txd, - enabled = enabled, + value = formState.value.txd, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { serialInput = serialInput.copy { txd = it } }, + onValueChanged = { formState.value = formState.value.copy { txd = it } }, ) } item { DropDownPreference( title = stringResource(R.string.serial_baud_rate), - enabled = enabled, + enabled = state.connected, items = SerialConfig.Serial_Baud.entries .filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED } .map { it to it.name }, - selectedItem = serialInput.baud, - onItemSelected = { serialInput = serialInput.copy { baud = it } }, + selectedItem = formState.value.baud, + onItemSelected = { formState.value = formState.value.copy { baud = it } }, ) } item { HorizontalDivider() } @@ -127,23 +114,23 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli item { EditTextPreference( title = stringResource(R.string.timeout), - value = serialInput.timeout, - enabled = enabled, + value = formState.value.timeout, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { serialInput = serialInput.copy { timeout = it } }, + onValueChanged = { formState.value = formState.value.copy { timeout = it } }, ) } item { DropDownPreference( title = stringResource(R.string.serial_mode), - enabled = enabled, + enabled = state.connected, items = SerialConfig.Serial_Mode.entries .filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED } .map { it to it.name }, - selectedItem = serialInput.mode, - onItemSelected = { serialInput = serialInput.copy { mode = it } }, + selectedItem = formState.value.mode, + onItemSelected = { formState.value = formState.value.copy { mode = it } }, ) } item { HorizontalDivider() } @@ -151,31 +138,11 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli item { SwitchPreference( title = stringResource(R.string.override_console_serial_port), - checked = serialInput.overrideConsoleSerialPort, - enabled = enabled, - onCheckedChange = { serialInput = serialInput.copy { overrideConsoleSerialPort = it } }, + checked = formState.value.overrideConsoleSerialPort, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { overrideConsoleSerialPort = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && serialInput != serialConfig, - onCancelClicked = { - focusManager.clearFocus() - serialInput = serialConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(serialInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun SerialConfigPreview() { - SerialConfigItemList(serialConfig = SerialConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt index f82f6dc6f..4abcdacaa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/StoreForwardConfigItemList.kt @@ -17,67 +17,50 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val storeForwardConfig = state.moduleConfig.storeForward + val formState = rememberConfigState(initialValue = storeForwardConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - StoreForwardConfigItemList( - storeForwardConfig = state.moduleConfig.storeForward, + RadioConfigScreenList( + title = stringResource(id = R.string.store_forward), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { storeForwardInput -> - val config = moduleConfig { storeForward = storeForwardInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { storeForward = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun StoreForwardConfigItemList( - storeForwardConfig: StoreForwardConfig, - enabled: Boolean, - onSaveClicked: (StoreForwardConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var storeForwardInput by rememberSaveable { mutableStateOf(storeForwardConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.store_forward_config)) } item { SwitchPreference( title = stringResource(R.string.store_forward_enabled), - checked = storeForwardInput.enabled, - enabled = enabled, - onCheckedChange = { storeForwardInput = storeForwardInput.copy { this.enabled = it } }, + checked = formState.value.enabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } }, ) } item { HorizontalDivider() } @@ -85,9 +68,9 @@ fun StoreForwardConfigItemList( item { SwitchPreference( title = stringResource(R.string.heartbeat), - checked = storeForwardInput.heartbeat, - enabled = enabled, - onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } }, + checked = formState.value.heartbeat, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { heartbeat = it } }, ) } item { HorizontalDivider() } @@ -95,65 +78,41 @@ fun StoreForwardConfigItemList( item { EditTextPreference( title = stringResource(R.string.number_of_records), - value = storeForwardInput.records, - enabled = enabled, + value = formState.value.records, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } }, + onValueChanged = { formState.value = formState.value.copy { records = it } }, ) } item { EditTextPreference( title = stringResource(R.string.history_return_max), - value = storeForwardInput.historyReturnMax, - enabled = enabled, + value = formState.value.historyReturnMax, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnMax = it } }, + onValueChanged = { formState.value = formState.value.copy { historyReturnMax = it } }, ) } item { EditTextPreference( title = stringResource(R.string.history_return_window), - value = storeForwardInput.historyReturnWindow, - enabled = enabled, + value = formState.value.historyReturnWindow, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnWindow = it } }, + onValueChanged = { formState.value = formState.value.copy { historyReturnWindow = it } }, ) } item { SwitchPreference( title = stringResource(R.string.server), - checked = storeForwardInput.isServer, - enabled = enabled, - onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } }, + checked = formState.value.isServer, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { isServer = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && storeForwardInput != storeForwardConfig, - onCancelClicked = { - focusManager.clearFocus() - storeForwardInput = storeForwardConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(storeForwardInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun StoreForwardConfigPreview() { - StoreForwardConfigItemList( - storeForwardConfig = StoreForwardConfig.getDefaultInstance(), - enabled = true, - onSaveClicked = {}, - ) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt index 44167c033..79aa3ea0d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/TelemetryConfigItemList.kt @@ -17,87 +17,70 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig +import androidx.navigation.NavController import com.geeksville.mesh.copy import com.geeksville.mesh.moduleConfig import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import org.meshtastic.core.strings.R @Composable -fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val telemetryConfig = state.moduleConfig.telemetry + val formState = rememberConfigState(initialValue = telemetryConfig) + val focusManager = LocalFocusManager.current - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - TelemetryConfigItemList( - telemetryConfig = state.moduleConfig.telemetry, + RadioConfigScreenList( + title = stringResource(id = R.string.telemetry), + onBack = { navController.popBackStack() }, + configState = formState, enabled = state.connected, - onSaveClicked = { telemetryInput -> - val config = moduleConfig { telemetry = telemetryInput } + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = moduleConfig { telemetry = it } viewModel.setModuleConfig(config) }, - ) -} - -@Composable -fun TelemetryConfigItemList( - telemetryConfig: TelemetryConfig, - enabled: Boolean, - onSaveClicked: (TelemetryConfig) -> Unit, -) { - val focusManager = LocalFocusManager.current - var telemetryInput by rememberSaveable { mutableStateOf(telemetryConfig) } - - LazyColumn(modifier = Modifier.fillMaxSize()) { + ) { item { PreferenceCategory(text = stringResource(R.string.telemetry_config)) } item { EditTextPreference( title = stringResource(R.string.device_metrics_update_interval_seconds), - value = telemetryInput.deviceUpdateInterval, - enabled = enabled, + value = formState.value.deviceUpdateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { telemetryInput = telemetryInput.copy { deviceUpdateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { deviceUpdateInterval = it } }, ) } item { EditTextPreference( title = stringResource(R.string.environment_metrics_update_interval_seconds), - value = telemetryInput.environmentUpdateInterval, - enabled = enabled, + value = formState.value.environmentUpdateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { telemetryInput = telemetryInput.copy { environmentUpdateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { environmentUpdateInterval = it } }, ) } item { SwitchPreference( title = stringResource(R.string.environment_metrics_module_enabled), - checked = telemetryInput.environmentMeasurementEnabled, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it } }, + checked = formState.value.environmentMeasurementEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { environmentMeasurementEnabled = it } }, ) } item { HorizontalDivider() } @@ -105,9 +88,9 @@ fun TelemetryConfigItemList( item { SwitchPreference( title = stringResource(R.string.environment_metrics_on_screen_enabled), - checked = telemetryInput.environmentScreenEnabled, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { environmentScreenEnabled = it } }, + checked = formState.value.environmentScreenEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { environmentScreenEnabled = it } }, ) } item { HorizontalDivider() } @@ -115,9 +98,9 @@ fun TelemetryConfigItemList( item { SwitchPreference( title = stringResource(R.string.environment_metrics_use_fahrenheit), - checked = telemetryInput.environmentDisplayFahrenheit, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it } }, + checked = formState.value.environmentDisplayFahrenheit, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { environmentDisplayFahrenheit = it } }, ) } item { HorizontalDivider() } @@ -125,9 +108,9 @@ fun TelemetryConfigItemList( item { SwitchPreference( title = stringResource(R.string.air_quality_metrics_module_enabled), - checked = telemetryInput.airQualityEnabled, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { airQualityEnabled = it } }, + checked = formState.value.airQualityEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { airQualityEnabled = it } }, ) } item { HorizontalDivider() } @@ -135,19 +118,19 @@ fun TelemetryConfigItemList( item { EditTextPreference( title = stringResource(R.string.air_quality_metrics_update_interval_seconds), - value = telemetryInput.airQualityInterval, - enabled = enabled, + value = formState.value.airQualityInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { telemetryInput = telemetryInput.copy { airQualityInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { airQualityInterval = it } }, ) } item { SwitchPreference( title = stringResource(R.string.power_metrics_module_enabled), - checked = telemetryInput.powerMeasurementEnabled, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it } }, + checked = formState.value.powerMeasurementEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { powerMeasurementEnabled = it } }, ) } item { HorizontalDivider() } @@ -155,41 +138,21 @@ fun TelemetryConfigItemList( item { EditTextPreference( title = stringResource(R.string.power_metrics_update_interval_seconds), - value = telemetryInput.powerUpdateInterval, - enabled = enabled, + value = formState.value.powerUpdateInterval, + enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { telemetryInput = telemetryInput.copy { powerUpdateInterval = it } }, + onValueChanged = { formState.value = formState.value.copy { powerUpdateInterval = it } }, ) } item { SwitchPreference( title = stringResource(R.string.power_metrics_on_screen_enabled), - checked = telemetryInput.powerScreenEnabled, - enabled = enabled, - onCheckedChange = { telemetryInput = telemetryInput.copy { powerScreenEnabled = it } }, + checked = formState.value.powerScreenEnabled, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { powerScreenEnabled = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && telemetryInput != telemetryConfig, - onCancelClicked = { - focusManager.clearFocus() - telemetryInput = telemetryConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(telemetryInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun TelemetryConfigPreview() { - TelemetryConfigItemList(telemetryConfig = TelemetryConfig.getDefaultInstance(), enabled = true, onSaveClicked = {}) -} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt index 4a149eb8e..ebd5703b2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/UserConfigItemList.kt @@ -17,32 +17,23 @@ package com.geeksville.mesh.ui.settings.radio.components -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.MeshProtos +import androidx.navigation.NavController import com.geeksville.mesh.copy -import com.geeksville.mesh.deviceMetadata import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.isUnmessageableRole import com.geeksville.mesh.ui.common.components.EditTextPreference import com.geeksville.mesh.ui.common.components.PreferenceCategory -import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.RegularPreference import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel @@ -50,74 +41,65 @@ import com.geeksville.mesh.user import org.meshtastic.core.strings.R @Composable -fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { +fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + val userConfig = state.userConfig + val formState = rememberConfigState(initialValue = userConfig) + val firmwareVersion = DeviceVersion(state.metadata?.firmwareVersion ?: "") - if (state.responseState.isWaiting()) { - PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse) - } - - UserConfigItemList( - userConfig = state.userConfig, - enabled = true, - onSaveClicked = viewModel::setOwner, - metadata = state.metadata, - ) -} - -@Suppress("LongMethod") -@Composable -fun UserConfigItemList( - metadata: MeshProtos.DeviceMetadata?, - userConfig: MeshProtos.User, - enabled: Boolean, - onSaveClicked: (MeshProtos.User) -> Unit, -) { - val focusManager = LocalFocusManager.current - var userInput by rememberSaveable { mutableStateOf(userConfig) } - val firmwareVersion = DeviceVersion(metadata?.firmwareVersion ?: "") - - val validLongName = userInput.longName.isNotBlank() - val validShortName = userInput.shortName.isNotBlank() + val validLongName = formState.value.longName.isNotBlank() + val validShortName = formState.value.shortName.isNotBlank() val validNames = validLongName && validShortName - LazyColumn(modifier = Modifier.fillMaxSize()) { + val focusManager = LocalFocusManager.current + + RadioConfigScreenList( + title = stringResource(id = R.string.user), + onBack = { navController.popBackStack() }, + configState = formState, + enabled = state.connected && validNames, + responseState = state.responseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = viewModel::setOwner, + ) { item { PreferenceCategory(text = stringResource(R.string.user_config)) } - item { RegularPreference(title = stringResource(R.string.node_id), subtitle = userInput.id, onClick = {}) } + item { + RegularPreference(title = stringResource(R.string.node_id), subtitle = formState.value.id, onClick = {}) + } item { HorizontalDivider() } item { EditTextPreference( title = stringResource(R.string.long_name), - value = userInput.longName, + value = formState.value.longName, maxSize = 39, // long_name max_size:40 - enabled = enabled, + enabled = state.connected, isError = !validLongName, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { userInput = userInput.copy { longName = it } }, + onValueChanged = { formState.value = formState.value.copy { longName = it } }, ) } item { EditTextPreference( title = stringResource(R.string.short_name), - value = userInput.shortName, + value = formState.value.shortName, maxSize = 4, // short_name max_size:5 - enabled = enabled, + enabled = state.connected, isError = !validShortName, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { userInput = userInput.copy { shortName = it } }, + onValueChanged = { formState.value = formState.value.copy { shortName = it } }, ) } item { RegularPreference( title = stringResource(R.string.hardware_model), - subtitle = userInput.hwModel.name, + subtitle = formState.value.hwModel.name, onClick = {}, ) } @@ -128,10 +110,10 @@ fun UserConfigItemList( title = stringResource(R.string.unmessageable), summary = stringResource(R.string.unmonitored_or_infrastructure), checked = - userInput.isUnmessagable || - (firmwareVersion < DeviceVersion("2.6.9") && userInput.role.isUnmessageableRole()), - enabled = userInput.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"), - onCheckedChange = { userInput = userInput.copy { isUnmessagable = it } }, + formState.value.isUnmessagable || + (firmwareVersion < DeviceVersion("2.6.9") && formState.value.role.isUnmessageableRole()), + enabled = formState.value.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"), + onCheckedChange = { formState.value = formState.value.copy { isUnmessagable = it } }, ) } @@ -141,43 +123,11 @@ fun UserConfigItemList( SwitchPreference( title = stringResource(R.string.licensed_amateur_radio), summary = stringResource(R.string.licensed_amateur_radio_text), - checked = userInput.isLicensed, - enabled = enabled, - onCheckedChange = { userInput = userInput.copy { isLicensed = it } }, + checked = formState.value.isLicensed, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { isLicensed = it } }, ) } item { HorizontalDivider() } - - item { - PreferenceFooter( - enabled = enabled && userInput != userConfig && validNames, - onCancelClicked = { - focusManager.clearFocus() - userInput = userConfig - }, - onSaveClicked = { - focusManager.clearFocus() - onSaveClicked(userInput) - }, - ) - } } } - -@Preview(showBackground = true) -@Composable -private fun UserConfigPreview() { - UserConfigItemList( - userConfig = - user { - id = "!a280d9c8" - longName = "Meshtastic d9c8" - shortName = "d9c8" - hwModel = MeshProtos.HardwareModel.RAK4631 - isLicensed = false - }, - enabled = true, - onSaveClicked = {}, - metadata = deviceMetadata { firmwareVersion = "2.8.0" }, - ) -} diff --git a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt index 1431185d2..1c39b0554 100644 --- a/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt +++ b/build-logic/convention/src/main/kotlin/com/geeksville/mesh/buildlogic/Detekt.kt @@ -30,7 +30,6 @@ internal fun Project.configureDetekt(extension: DetektExtension) = extension.app config.setFrom("$rootDir/config/detekt/detekt.yml") buildUponDefaultConfig = true allRules = false - baseline = file("$rootDir/config/detekt/baseline.xml") source.setFrom( files( "src/main/java", diff --git a/config/detekt/detekt-baseline-meshserviceexample.xml b/config/detekt/detekt-baseline-meshserviceexample.xml deleted file mode 100644 index b577057f4..000000000 --- a/config/detekt/detekt-baseline-meshserviceexample.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - CommentSpacing:NodeInfo.kt$NodeInfo$/// @return a nice human readable string for the distance, or null for unknown - CommentSpacing:NodeInfo.kt$NodeInfo$/// @return bearing to the other position in degrees - CommentSpacing:NodeInfo.kt$NodeInfo$/// @return distance in meters to some other node (or null if unknown) - CommentSpacing:NodeInfo.kt$NodeInfo$/// return the position if it is valid, else null - CommentSpacing:NodeInfo.kt$Position$/// @return bearing to the other position in degrees - CommentSpacing:NodeInfo.kt$Position$/// @return distance in meters to some other node (or null if unknown) - CommentSpacing:NodeInfo.kt$Position.Companion$/// Convert to a double representation of degrees - FinalNewline:DataPacket.kt$com.geeksville.mesh.DataPacket.kt - FinalNewline:MyNodeInfo.kt$com.geeksville.mesh.MyNodeInfo.kt - FinalNewline:NodeInfo.kt$com.geeksville.mesh.NodeInfo.kt - FunctionParameterNaming:LocationUtils.kt$_degIn: Double - FunctionParameterNaming:LocationUtils.kt$lat_a: Double - FunctionParameterNaming:LocationUtils.kt$lat_b: Double - FunctionParameterNaming:LocationUtils.kt$lng_a: Double - FunctionParameterNaming:LocationUtils.kt$lng_b: Double - ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format( "%s%s %.6s %.7s", UTM.zone, UTM.toMGRS().band, UTM.easting, UTM.northing ) - ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format( "%s%s %s%s %05d %05d", MGRS.zone, MGRS.band, MGRS.column, MGRS.row, MGRS.easting, MGRS.northing ) - ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format("%.5f %.5f", p.latitude, p.longitude) - ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format("%s°%s'%.5s\"%s", a[0], a[1], a[2], a[3]) - ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel) - MagicNumber:DataPacket.kt$DataPacket.CREATOR$16 - MagicNumber:Extensions.kt$1000 - MagicNumber:Extensions.kt$1440000 - MagicNumber:Extensions.kt$24 - MagicNumber:Extensions.kt$2880 - MagicNumber:Extensions.kt$60 - MagicNumber:LocationUtils.kt$0.8 - MagicNumber:LocationUtils.kt$110540 - MagicNumber:LocationUtils.kt$111320 - MagicNumber:LocationUtils.kt$180 - MagicNumber:LocationUtils.kt$1e-7 - MagicNumber:LocationUtils.kt$360 - MagicNumber:LocationUtils.kt$360.0 - MagicNumber:LocationUtils.kt$3600.0 - MagicNumber:LocationUtils.kt$60 - MagicNumber:LocationUtils.kt$60.0 - MagicNumber:LocationUtils.kt$6366000 - MagicNumber:LocationUtils.kt$GPSFormat$3 - MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000 - MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000 - MagicNumber:NodeInfo.kt$NodeInfo$0.114 - MagicNumber:NodeInfo.kt$NodeInfo$0.299 - MagicNumber:NodeInfo.kt$NodeInfo$0.587 - MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF - MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00 - MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000 - MagicNumber:NodeInfo.kt$NodeInfo$1000 - MagicNumber:NodeInfo.kt$NodeInfo$1000.0 - MagicNumber:NodeInfo.kt$NodeInfo$15 - MagicNumber:NodeInfo.kt$NodeInfo$16 - MagicNumber:NodeInfo.kt$NodeInfo$1609 - MagicNumber:NodeInfo.kt$NodeInfo$1609.34 - MagicNumber:NodeInfo.kt$NodeInfo$255 - MagicNumber:NodeInfo.kt$NodeInfo$3.281 - MagicNumber:NodeInfo.kt$NodeInfo$60 - MagicNumber:NodeInfo.kt$NodeInfo$8 - MagicNumber:NodeInfo.kt$Position$180 - MagicNumber:NodeInfo.kt$Position$90 - MagicNumber:NodeInfo.kt$Position$90.0 - MagicNumber:NodeInfo.kt$Position.Companion$1000 - MagicNumber:NodeInfo.kt$Position.Companion$1e-7 - MagicNumber:NodeInfo.kt$Position.Companion$1e7 - MatchingDeclarationName:LocationUtils.kt$GPSFormat - MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int - MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist < 1609 -> "%.0f ft".format(dist.toDouble()*3.281) - MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist >= 1609 -> "%.1f mi".format(dist / 1609.34) - MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist < 1000 -> "%.0f m".format(dist.toDouble()) - MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist >= 1000 -> "%.1f km".format(dist / 1000.0) - MaxLineLength:NodeInfo.kt$Position$/** - MaxLineLength:NodeInfo.kt$Position$return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time})" - MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase() - MultiLineIfElse:NodeInfo.kt$MeshUser$null - NewLineAtEndOfFile:DataPacket.kt$com.geeksville.mesh.DataPacket.kt - NewLineAtEndOfFile:MyNodeInfo.kt$com.geeksville.mesh.MyNodeInfo.kt - NewLineAtEndOfFile:NodeInfo.kt$com.geeksville.mesh.NodeInfo.kt - NoConsecutiveBlankLines:NodeInfo.kt$ - SpacingAroundOperators:NodeInfo.kt$NodeInfo$* - StringTemplate:NodeInfo.kt$Position$${time} - TooManyFunctions:LocationUtils.kt$com.geeksville.mesh.util.LocationUtils.kt - - diff --git a/config/detekt/detekt-baseline.xml b/config/detekt/detekt-baseline.xml deleted file mode 100644 index 3d38b69f7..000000000 --- a/config/detekt/detekt-baseline.xml +++ /dev/null @@ -1,378 +0,0 @@ - - - - TooManyFunctions:ContactSharing.kt$com.geeksville.mesh.ui.ContactSharing.kt - TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.NodeDetail.kt - - - ChainWrapping:Channel.kt$Channel$&& - CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down - CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back - CommentSpacing:ContextExtensions.kt$/// Utility function to hide the soft keyboard per stack overflow - CommentSpacing:ContextExtensions.kt$/// show a toast - CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib - CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work - CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work - CommentSpacing:Exceptions.kt$/// Convert any exceptions in this service call into a RemoteException that the client can - CommentSpacing:Exceptions.kt$/// then handle - CommentSpacing:Exceptions.kt$Exceptions$/// Set in Application.onCreate - CommentSpacing:MockInterface.kt$MockInterface$/// Generate a fake node info entry - CommentSpacing:MockInterface.kt$MockInterface$/// Generate a fake text message from a node - CommentSpacing:MockInterface.kt$MockInterface$/// Send a fake ack packet back if the sender asked for want_ack - ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "message") val raw_message: String - ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "received_date") val received_date: Long - ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "type") val message_type: String - ConstructorParameterNaming:Packet.kt$ContactSettings$@PrimaryKey val contact_key: String - ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String - ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int - ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long - CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) - CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) - EmptyCatchBlock:MeshLog.kt$MeshLog${ } - EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ } - EmptyFunctionBlock:NopInterface.kt$NopInterface${ } - EmptyFunctionBlock:NsdManager.kt$<no name provided>${ } - EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${} - FinalNewline:AppPrefs.kt$com.geeksville.mesh.android.AppPrefs.kt - FinalNewline:ApplicationModule.kt$com.geeksville.mesh.ApplicationModule.kt - FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt - FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt - FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt - FinalNewline:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt - FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt - FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt - FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt - FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt - FinalNewline:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt - FinalNewline:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt - FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt - FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt - FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt - FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt - FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt - FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt - FinalNewline:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt - FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt - FinalNewline:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt - FinalNewline:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt - FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt - FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt - FinalNewline:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt - FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt - FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt - FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt - FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt - ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE - FunctionNaming:PacketDao.kt$PacketDao$@Query("DELETE FROM packet WHERE uuid=:uuid") suspend fun _delete(uuid: Long) - FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long) - ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel) - LargeClass:MeshService.kt$MeshService : ServiceLogging - LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigItemList( ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, enabled: Boolean, onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit, ) - LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigItemList( audioConfig: AudioConfig, enabled: Boolean, onSaveClicked: (AudioConfig) -> Unit, ) - LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigItemList( messages: String, cannedMessageConfig: CannedMessageConfig, enabled: Boolean, onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit, ) - LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigItemList( deviceConfig: DeviceConfig, enabled: Boolean, onSaveClicked: (DeviceConfig) -> Unit, ) - LongMethod:DropDownPreference.kt$@Composable fun <T> DropDownPreference( title: String, enabled: Boolean, items: List<Pair<T, String>>, selectedItem: T, onItemSelected: (T) -> Unit, modifier: Modifier = Modifier, summary: String? = null, ) - LongMethod:EditListPreference.kt$@Composable inline fun <reified T> EditListPreference( title: String, list: List<T>, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List<T>) -> Unit, modifier: Modifier = Modifier, ) - LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, ) - LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) - LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) - LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit, ) - LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigItemList( storeForwardConfig: StoreForwardConfig, enabled: Boolean, onSaveClicked: (StoreForwardConfig) -> Unit, ) - LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigItemList( telemetryConfig: TelemetryConfig, enabled: Boolean, onSaveClicked: (TelemetryConfig) -> Unit, ) - LongParameterList:NOAAWmsTileSource.kt$NOAAWmsTileSource$( aName: String, aBaseUrl: Array<String>, layername: String, version: String, time: String?, srs: String, style: String?, format: String, ) - LongParameterList:RadioInterfaceService.kt$RadioInterfaceService$( private val context: Application, private val dispatchers: CoroutineDispatchers, private val bluetoothRepository: BluetoothRepository, private val networkRepository: NetworkRepository, private val processLifecycle: Lifecycle, @RadioRepositoryQualifier private val prefs: SharedPreferences, private val interfaceFactory: InterfaceFactory, ) - MagicNumber:BatteryInfo.kt$100 - MagicNumber:BatteryInfo.kt$101 - MagicNumber:BatteryInfo.kt$14 - MagicNumber:BatteryInfo.kt$15 - MagicNumber:BatteryInfo.kt$34 - MagicNumber:BatteryInfo.kt$35 - MagicNumber:BatteryInfo.kt$4 - MagicNumber:BatteryInfo.kt$5 - MagicNumber:BatteryInfo.kt$79 - MagicNumber:BatteryInfo.kt$80 - MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000 - MagicNumber:BluetoothInterface.kt$BluetoothInterface$500 - MagicNumber:BluetoothInterface.kt$BluetoothInterface$512 - MagicNumber:Channel.kt$0xff - MagicNumber:ChannelOption.kt$.03125f - MagicNumber:ChannelOption.kt$.0625f - MagicNumber:ChannelOption.kt$.203125f - MagicNumber:ChannelOption.kt$.40625f - MagicNumber:ChannelOption.kt$.8125f - MagicNumber:ChannelOption.kt$1.6250f - MagicNumber:ChannelOption.kt$1000f - MagicNumber:ChannelOption.kt$1600 - MagicNumber:ChannelOption.kt$200 - MagicNumber:ChannelOption.kt$3.25f - MagicNumber:ChannelOption.kt$31 - MagicNumber:ChannelOption.kt$400 - MagicNumber:ChannelOption.kt$5 - MagicNumber:ChannelOption.kt$62 - MagicNumber:ChannelOption.kt$800 - MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f - MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f - MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f - MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f - MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f - MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f - MagicNumber:ChannelSet.kt$40 - MagicNumber:ChannelSet.kt$960 - MagicNumber:Contacts.kt$7 - MagicNumber:Contacts.kt$8 - MagicNumber:DataPacket.kt$DataPacket.CREATOR$16 - MagicNumber:Debug.kt$3 - MagicNumber:DeviceVersion.kt$DeviceVersion$100 - MagicNumber:DeviceVersion.kt$DeviceVersion$10000 - MagicNumber:DownloadButton.kt$1.25f - MagicNumber:EditChannelDialog.kt$16 - MagicNumber:EditChannelDialog.kt$32 - MagicNumber:EditIPv4Preference.kt$0xff - MagicNumber:EditIPv4Preference.kt$16 - MagicNumber:EditIPv4Preference.kt$24 - MagicNumber:EditIPv4Preference.kt$8 - MagicNumber:EditListPreference.kt$12 - MagicNumber:EditListPreference.kt$12345 - MagicNumber:EditListPreference.kt$67890 - MagicNumber:Extensions.kt$1000 - MagicNumber:Extensions.kt$1440000 - MagicNumber:Extensions.kt$24 - MagicNumber:Extensions.kt$2880 - MagicNumber:Extensions.kt$60 - MagicNumber:LazyColumnDragAndDropDemo.kt$50 - MagicNumber:LocationRepository.kt$LocationRepository$1000L - MagicNumber:LocationRepository.kt$LocationRepository$30 - MagicNumber:LocationRepository.kt$LocationRepository$31 - MagicNumber:LocationUtils.kt$1e-7 - MagicNumber:LocationUtils.kt$360 - MagicNumber:MQTTRepository.kt$MQTTRepository$512 - MagicNumber:MapView.kt$0.5f - MagicNumber:MapView.kt$1.3 - MagicNumber:MapView.kt$1024.0 - MagicNumber:MapView.kt$128205 - MagicNumber:MapView.kt$12F - MagicNumber:MapView.kt$<no name provided>$1e7 - MagicNumber:MapViewExtensions.kt$1e-5 - MagicNumber:MapViewExtensions.kt$1e-7 - MagicNumber:MapViewExtensions.kt$3.0f - MagicNumber:MapViewExtensions.kt$40f - MagicNumber:MapViewExtensions.kt$60f - MagicNumber:MapViewExtensions.kt$80f - MagicNumber:MarkerWithLabel.kt$MarkerWithLabel$3 - MagicNumber:MeshService.kt$MeshService$0xffffffff - MagicNumber:MeshService.kt$MeshService$100 - MagicNumber:MeshService.kt$MeshService$1000 - MagicNumber:MeshService.kt$MeshService$1000.0 - MagicNumber:MeshService.kt$MeshService$1000L - MagicNumber:MeshService.kt$MeshService$16 - MagicNumber:MeshService.kt$MeshService$30 - MagicNumber:MeshService.kt$MeshService$32 - MagicNumber:MeshService.kt$MeshService$60000 - MagicNumber:MeshService.kt$MeshService$8 - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5 - MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7 - MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$180 - MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$256 - MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$360.0 - MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$4 - MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$5 - MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000 - MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000 - MagicNumber:NodeInfo.kt$NodeInfo$0.114 - MagicNumber:NodeInfo.kt$NodeInfo$0.299 - MagicNumber:NodeInfo.kt$NodeInfo$0.587 - MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF - MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00 - MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000 - MagicNumber:NodeInfo.kt$NodeInfo$1000 - MagicNumber:NodeInfo.kt$NodeInfo$1000.0 - MagicNumber:NodeInfo.kt$NodeInfo$16 - MagicNumber:NodeInfo.kt$NodeInfo$1609 - MagicNumber:NodeInfo.kt$NodeInfo$1609.34 - MagicNumber:NodeInfo.kt$NodeInfo$255 - MagicNumber:NodeInfo.kt$NodeInfo$3.281 - MagicNumber:NodeInfo.kt$NodeInfo$8 - MagicNumber:NodeInfo.kt$Position$180 - MagicNumber:NodeInfo.kt$Position$90 - MagicNumber:NodeInfo.kt$Position$90.0 - MagicNumber:NodeInfo.kt$Position.Companion$1000 - MagicNumber:NodeInfo.kt$Position.Companion$1e-7 - MagicNumber:NodeInfo.kt$Position.Companion$1e7 - MagicNumber:PacketRepository.kt$PacketRepository$500 - MagicNumber:PacketResponseStateDialog.kt$100 - MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972 - MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809 - MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790 - MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114 - MagicNumber:SafeBluetooth.kt$SafeBluetooth$10 - MagicNumber:SafeBluetooth.kt$SafeBluetooth$100 - MagicNumber:SafeBluetooth.kt$SafeBluetooth$1000 - MagicNumber:SafeBluetooth.kt$SafeBluetooth$2500 - MagicNumber:SafeBluetooth.kt$SafeBluetooth.<no name provided>$2500 - MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200 - MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200 - MagicNumber:ServiceClient.kt$ServiceClient$500 - MagicNumber:StreamInterface.kt$StreamInterface$0xff - MagicNumber:StreamInterface.kt$StreamInterface$3 - MagicNumber:StreamInterface.kt$StreamInterface$4 - MagicNumber:StreamInterface.kt$StreamInterface$8 - MagicNumber:TCPInterface.kt$TCPInterface$1000 - MagicNumber:TCPInterface.kt$TCPInterface$180 - MagicNumber:TCPInterface.kt$TCPInterface$500 - MagicNumber:UIState.kt$4 - MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider - MatchingDeclarationName:DistanceExtensions.kt$DistanceUnit - MatchingDeclarationName:LocationUtils.kt$GPSFormat - MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker - MatchingDeclarationName:SortOption.kt$NodeSortOption - MaxLineLength:AppPrefs.kt$FloatPref$fun get(thisRef: AppPrefs, prop: KProperty<Float>): Float - MaxLineLength:AppPrefs.kt$StringPref$fun get(thisRef: AppPrefs, prop: KProperty<String>): String - MaxLineLength:BluetoothInterface.kt$/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface. MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there. FIXME - make sure this protocol is guaranteed robust and won't drop packets "According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)). In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes." MAXPACKET is 256? look into what the lora lib uses. FIXME Characteristics: UUID properties description 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 read fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet). After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this mailbox. f75c76d2-129e-4dad-a1dd-7866124401e7 write toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len) ed9da18c-a800-4f66-a670-aa7547e34453 read|notify|write fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages until it catches up with this number. The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32 callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio. When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio. Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted. Re: queue management Not all messages are kept in the fromradio queue (filtered based on SubPacket): * only the most recent Position and User messages for a particular node are kept * all Data SubPackets are kept * No WantNodeNum / DenyNodeNum messages are kept A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging) */ - MaxLineLength:BluetoothState.kt$BluetoothState$"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})" - MaxLineLength:Channel.kt$Channel$// We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name - MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int - MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq - MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m") - MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$* - MaxLineLength:ServiceClient.kt$ServiceClient$// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try - MaxLineLength:ServiceClient.kt$ServiceClient.<no name provided>$// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue - MaxLineLength:StreamInterface.kt$StreamInterface$* - MaxLineLength:StreamInterface.kt$StreamInterface$* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP probably) - MaxLineLength:StreamInterface.kt$StreamInterface$// Note: we have to check if ptr +1 is equal to packet length (for example, for a 1 byte packetlen, this code will be run with ptr of4 - MaxLineLength:StreamInterface.kt$StreamInterface$deliverPacket() - MaxLineLength:StreamInterface.kt$StreamInterface$lostSync() - MaxLineLength:StreamInterface.kt$StreamInterface$service.onDisconnect(isPermanent = true) - MayBeConst:AppPrefs.kt$AppPrefs.Companion$private val baseName = "appPrefs_" - MultiLineIfElse:Channel.kt$Channel$"Custom" - MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" } - MultiLineIfElse:EditListPreference.kt$EditBase64Preference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChange = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) - MultiLineIfElse:EditListPreference.kt$EditTextPreference( title = "${index + 1}/$maxCount", value = value, enabled = enabled, keyboardActions = keyboardActions, onValueChanged = { listState[index] = it as T onValuesChanged(listState) }, modifier = modifier.fillMaxWidth(), trailingIcon = trailingIcon, ) - MultiLineIfElse:EditTextPreference.kt$it.toDoubleOrNull()?.let { double -> valueState = it onValueChanged(double) } - MultiLineIfElse:EditTextPreference.kt$it.toFloatOrNull()?.let { float -> valueState = it onValueChanged(float) } - MultiLineIfElse:EditTextPreference.kt$it.toUIntOrNull()?.toInt()?.let { int -> valueState = it onValueChanged(int) } - MultiLineIfElse:EditTextPreference.kt$onValueChanged(it) - MultiLineIfElse:EditTextPreference.kt$valueState = it - MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex) - NestedBlockDepth:LanguageUtils.kt$LanguageUtils$fun getLanguageTags(context: Context): Map<String, String> - NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) - NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket) - NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) - NewLineAtEndOfFile:AppPrefs.kt$com.geeksville.mesh.android.AppPrefs.kt - NewLineAtEndOfFile:ApplicationModule.kt$com.geeksville.mesh.ApplicationModule.kt - NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt - NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt - NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt - NewLineAtEndOfFile:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt - NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt - NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt - NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt - NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt - NewLineAtEndOfFile:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt - NewLineAtEndOfFile:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt - NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt - NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt - NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt - NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt - NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt - NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt - NewLineAtEndOfFile:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt - NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt - NewLineAtEndOfFile:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt - NewLineAtEndOfFile:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt - NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt - NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt - NewLineAtEndOfFile:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt - NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt - NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt - NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt - NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt - NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ - NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ - NoConsecutiveBlankLines:AppPrefs.kt$ - NoConsecutiveBlankLines:BootCompleteReceiver.kt$ - NoConsecutiveBlankLines:Constants.kt$ - NoConsecutiveBlankLines:DebugLogFile.kt$ - NoConsecutiveBlankLines:DeferredExecution.kt$ - NoConsecutiveBlankLines:Exceptions.kt$ - NoConsecutiveBlankLines:IRadioInterface.kt$ - NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ } - NoSemicolons:DateUtils.kt$DateUtils$; - NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.* - NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.* - OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract - ParameterListWrapping:AppPrefs.kt$FloatPref$(thisRef: AppPrefs, prop: KProperty<Float>) - ParameterListWrapping:AppPrefs.kt$StringPref$(thisRef: AppPrefs, prop: KProperty<String>) - RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex - ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket) - SpacingAroundCurly:AppPrefs.kt$FloatPref$} - SpacingAroundKeyword:AppPrefs.kt$AppPrefs$if - SpacingAroundKeyword:Exceptions.kt$if - SpacingAroundKeyword:Exceptions.kt$when - SpacingAroundRangeOperator:BatteryInfo.kt$.. - SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException - SwallowedException:ChannelSet.kt$ex: Throwable - SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception - SwallowedException:Exceptions.kt$ex: Throwable - SwallowedException:MeshLog.kt$MeshLog$e: IOException - SwallowedException:MeshService.kt$MeshService$e: Exception - SwallowedException:MeshService.kt$MeshService$e: TimeoutException - SwallowedException:MeshService.kt$MeshService$ex: BLEException - SwallowedException:MeshService.kt$MeshService$ex: CancellationException - SwallowedException:NsdManager.kt$ex: IllegalArgumentException - SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: DeadObjectException - SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException - SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException - SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException - TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable - TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception - TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable - TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception - TooGenericExceptionCaught:Exceptions.kt$ex: Throwable - TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception - TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception - TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception - TooGenericExceptionCaught:MainActivity.kt$MainActivity$ex: Exception - TooGenericExceptionCaught:MapView.kt$ex: Exception - TooGenericExceptionCaught:MapViewModel.kt$MapViewModel$e: Exception - TooGenericExceptionCaught:MeshService.kt$MeshService$e: Exception - TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception - TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$ex: Exception - TooGenericExceptionCaught:MeshServiceStarter.kt$ServiceStarter$ex: Exception - TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception - TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: Exception - TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException - TooGenericExceptionCaught:SqlTileWriterExt.kt$SqlTileWriterExt$e: Exception - TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable - TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable - TooGenericExceptionThrown:DeviceVersion.kt$DeviceVersion$throw Exception("Can't parse version $s") - TooGenericExceptionThrown:MeshService.kt$MeshService$throw Exception("Can't set user without a NodeInfo") - TooGenericExceptionThrown:MeshService.kt$MeshService.<no name provided>$throw Exception("Port numbers must be non-zero!") - TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect") - TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound") - TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout") - TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen") - TooManyFunctions:AppPrefs.kt$AppPrefs - TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterfaceLogging - TooManyFunctions:MainActivity.kt$MainActivity : AppCompatActivityLogging - TooManyFunctions:MeshService.kt$MeshService : ServiceLogging - TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub - TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt - TooManyFunctions:PacketDao.kt$PacketDao - TooManyFunctions:PacketRepository.kt$PacketRepository - TooManyFunctions:RadioConfigRepository.kt$RadioConfigRepository - TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModelLogging - TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService : Logging - TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable - TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging - TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh" - UnusedPrivateMember:NOAAWmsTileSource.kt$NOAAWmsTileSource$private fun tile2lat(y: Int, z: Int): Double - UnusedPrivateMember:NOAAWmsTileSource.kt$NOAAWmsTileSource$private fun tile2lon(x: Int, z: Int): Double - UtilityClassWithPublicConstructor:CustomTileSource.kt$CustomTileSource - UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule - WildcardImport:MockInterface.kt$import com.geeksville.mesh.* - WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.* - - diff --git a/core/model/detekt-baseline.xml b/core/model/detekt-baseline.xml new file mode 100644 index 000000000..cc5067da4 --- /dev/null +++ b/core/model/detekt-baseline.xml @@ -0,0 +1,30 @@ + + + + + MagicNumber:Channel.kt$0xff + MagicNumber:ChannelOption.kt$.03125f + MagicNumber:ChannelOption.kt$.0625f + MagicNumber:ChannelOption.kt$.203125f + MagicNumber:ChannelOption.kt$.40625f + MagicNumber:ChannelOption.kt$.8125f + MagicNumber:ChannelOption.kt$1.6250f + MagicNumber:ChannelOption.kt$1000f + MagicNumber:ChannelOption.kt$1600 + MagicNumber:ChannelOption.kt$200 + MagicNumber:ChannelOption.kt$3.25f + MagicNumber:ChannelOption.kt$31 + MagicNumber:ChannelOption.kt$400 + MagicNumber:ChannelOption.kt$5 + MagicNumber:ChannelOption.kt$62 + MagicNumber:ChannelOption.kt$800 + MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f + MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f + MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f + MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f + MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f + MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f + MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f + MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f + + diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml index 3df9c88d5..ecc4caeeb 100644 --- a/core/strings/src/main/res/values/strings.xml +++ b/core/strings/src/main/res/values/strings.xml @@ -898,4 +898,5 @@ Icon Meanings Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required. Device configuration + "[Remote] %1$s" diff --git a/mesh_service_example/detekt-baseline.xml b/mesh_service_example/detekt-baseline.xml new file mode 100644 index 000000000..61cefa793 --- /dev/null +++ b/mesh_service_example/detekt-baseline.xml @@ -0,0 +1,37 @@ + + + + + ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel) + MagicNumber:DataPacket.kt$DataPacket.CREATOR$16 + MagicNumber:Extensions.kt$1000 + MagicNumber:Extensions.kt$1440000 + MagicNumber:Extensions.kt$24 + MagicNumber:Extensions.kt$2880 + MagicNumber:Extensions.kt$60 + MagicNumber:LocationUtils.kt$1e-7 + MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000 + MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000 + MagicNumber:NodeInfo.kt$NodeInfo$0.114 + MagicNumber:NodeInfo.kt$NodeInfo$0.299 + MagicNumber:NodeInfo.kt$NodeInfo$0.587 + MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF + MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00 + MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000 + MagicNumber:NodeInfo.kt$NodeInfo$1000 + MagicNumber:NodeInfo.kt$NodeInfo$1000.0 + MagicNumber:NodeInfo.kt$NodeInfo$16 + MagicNumber:NodeInfo.kt$NodeInfo$1609 + MagicNumber:NodeInfo.kt$NodeInfo$1609.34 + MagicNumber:NodeInfo.kt$NodeInfo$255 + MagicNumber:NodeInfo.kt$NodeInfo$3.281 + MagicNumber:NodeInfo.kt$NodeInfo$8 + MagicNumber:NodeInfo.kt$Position$180 + MagicNumber:NodeInfo.kt$Position$90 + MagicNumber:NodeInfo.kt$Position$90.0 + MagicNumber:NodeInfo.kt$Position.Companion$1000 + MagicNumber:NodeInfo.kt$Position.Companion$1e-7 + MagicNumber:NodeInfo.kt$Position.Companion$1e7 + MatchingDeclarationName:LocationUtils.kt$GPSFormat + +