refactor(settings)!: standardize radio config screens (#3167)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2025-09-22 21:59:33 -05:00
committed by GitHub
parent d2db37e0d4
commit ddb19b959f
35 changed files with 1480 additions and 2651 deletions

View File

@@ -2,11 +2,8 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ChainWrapping:Channel.kt$Channel$&amp;&amp;</ID>
<ID>CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down</ID>
<ID>CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back</ID>
<ID>CommentSpacing:ContextExtensions.kt$/// Utility function to hide the soft keyboard per stack overflow</ID>
<ID>CommentSpacing:ContextExtensions.kt$/// show a toast</ID>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work</ID>
@@ -18,8 +15,6 @@
<ID>ComposableParamOrder:AlertDialogs.kt$SimpleAlertDialog</ID>
<ID>ComposableParamOrder:BatteryInfo.kt$BatteryInfo</ID>
<ID>ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList</ID>
<ID>ComposableParamOrder:Connections.kt$ConnectionsScreen</ID>
<ID>ComposableParamOrder:CurrentlyConnectedCard.kt$CurrentlyConnectedCard</ID>
<ID>ComposableParamOrder:Debug.kt$DebugMenuActions</ID>
<ID>ComposableParamOrder:Debug.kt$DecodedPayloadBlock</ID>
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchState</ID>
@@ -51,7 +46,6 @@
<ID>ComposableParamOrder:NodeDetail.kt$EnvironmentMetrics</ID>
<ID>ComposableParamOrder:NodeDetail.kt$NodeActionButton</ID>
<ID>ComposableParamOrder:NodeDetail.kt$NodeDetailList</ID>
<ID>ComposableParamOrder:NodeDetail.kt$NodeDetailScreen</ID>
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
@@ -59,11 +53,9 @@
<ID>ComposableParamOrder:NodeScreen.kt$NodeScreen</ID>
<ID>ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart</ID>
<ID>ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout</ID>
<ID>ComposableParamOrder:PositionConfigItemList.kt$PositionConfigItemList</ID>
<ID>ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart</ID>
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
<ID>ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo</ID>
<ID>ComposableParamOrder:SecurityConfigItemList.kt$SecurityConfigItemList</ID>
<ID>ComposableParamOrder:SettingsItem.kt$SettingsItem</ID>
<ID>ComposableParamOrder:SignalInfo.kt$SignalInfo</ID>
<ID>ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart</ID>
@@ -80,6 +72,8 @@
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt$second</ID>
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>CyclomaticComplexMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>CyclomaticComplexMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>EmptyCatchBlock:MeshLog.kt$MeshLog${ }</ID>
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
@@ -117,7 +111,6 @@
<ID>FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long)</ID>
<ID>ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel)</ID>
<ID>LambdaParameterEventTrailing:Channel.kt$onConfirm</ID>
<ID>LambdaParameterEventTrailing:CurrentlyConnectedCard.kt$onClickDisconnect</ID>
<ID>LambdaParameterEventTrailing:MainAppBar.kt$onAction</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
@@ -128,15 +121,24 @@
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
<ID>LargeClass:MeshService.kt$MeshService : ServiceLogging</ID>
<ID>LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigItemList( ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, enabled: Boolean, onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -&gt; Unit, )</ID>
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigItemList( messages: String, cannedMessageConfig: CannedMessageConfig, enabled: Boolean, onSaveClicked: (messages: String, config: CannedMessageConfig) -&gt; Unit, )</ID>
<ID>LongMethod:DropDownPreference.kt$@Composable fun &lt;T&gt; DropDownPreference( title: String, enabled: Boolean, items: List&lt;Pair&lt;T, String&gt;&gt;, selectedItem: T, onItemSelected: (T) -&gt; Unit, modifier: Modifier = Modifier, summary: String? = null, )</ID>
<ID>LongMethod:EditListPreference.kt$@Composable inline fun &lt;reified T&gt; EditListPreference( title: String, list: List&lt;T&gt;, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List&lt;T&gt;) -&gt; Unit, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -&gt; Unit, )</ID>
<ID>LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigItemList( storeForwardConfig: StoreForwardConfig, enabled: Boolean, onSaveClicked: (StoreForwardConfig) -&gt; Unit, )</ID>
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigItemList( telemetryConfig: TelemetryConfig, enabled: Boolean, onSaveClicked: (TelemetryConfig) -&gt; Unit, )</ID>
<ID>LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>LongMethod:UserConfigItemList.kt$@Composable fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())</ID>
<ID>MagicNumber:BatteryInfo.kt$100</ID>
<ID>MagicNumber:BatteryInfo.kt$101</ID>
<ID>MagicNumber:BatteryInfo.kt$14</ID>
@@ -150,30 +152,6 @@
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$500</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$512</ID>
<ID>MagicNumber:Channel.kt$0xff</ID>
<ID>MagicNumber:ChannelOption.kt$.03125f</ID>
<ID>MagicNumber:ChannelOption.kt$.0625f</ID>
<ID>MagicNumber:ChannelOption.kt$.203125f</ID>
<ID>MagicNumber:ChannelOption.kt$.40625f</ID>
<ID>MagicNumber:ChannelOption.kt$.8125f</ID>
<ID>MagicNumber:ChannelOption.kt$1.6250f</ID>
<ID>MagicNumber:ChannelOption.kt$1000f</ID>
<ID>MagicNumber:ChannelOption.kt$1600</ID>
<ID>MagicNumber:ChannelOption.kt$200</ID>
<ID>MagicNumber:ChannelOption.kt$3.25f</ID>
<ID>MagicNumber:ChannelOption.kt$31</ID>
<ID>MagicNumber:ChannelOption.kt$400</ID>
<ID>MagicNumber:ChannelOption.kt$5</ID>
<ID>MagicNumber:ChannelOption.kt$62</ID>
<ID>MagicNumber:ChannelOption.kt$800</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f</ID>
<ID>MagicNumber:ChannelSet.kt$40</ID>
<ID>MagicNumber:ChannelSet.kt$960</ID>
<ID>MagicNumber:Contacts.kt$7</ID>
@@ -239,6 +217,7 @@
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e7</ID>
<ID>MagicNumber:PacketRepository.kt$PacketRepository$500</ID>
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
<ID>MagicNumber:PowerConfigItemList.kt$3600</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
@@ -266,7 +245,6 @@
<ID>MatchingDeclarationName:SortOption.kt$NodeSortOption</ID>
<ID>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 &gt;= 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) */</ID>
<ID>MaxLineLength:BluetoothState.kt$BluetoothState$"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})"</ID>
<ID>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</ID>
<ID>MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int</ID>
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
<ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID>
@@ -275,65 +253,49 @@
<ID>ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)</ID>
<ID>ModifierListSpacing:Packet.kt$Packet$@Entity( tableName = "packet", indices = [ Index(value = ["myNodeNum"]), Index(value = ["port_num"]), Index(value = ["contact_key"]), ] ) data</ID>
<ID>ModifierMissing:AdaptiveTwoPane.kt$AdaptiveTwoPane</ID>
<ID>ModifierMissing:AmbientLightingConfigItemList.kt$AmbientLightingConfigItemList</ID>
<ID>ModifierMissing:AudioConfigItemList.kt$AudioConfigItemList</ID>
<ID>ModifierMissing:BLEDevices.kt$BLEDevices</ID>
<ID>ModifierMissing:BluetoothConfigItemList.kt$BluetoothConfigItemList</ID>
<ID>ModifierMissing:CannedMessageConfigItemList.kt$CannedMessageConfigItemList</ID>
<ID>ModifierMissing:Channel.kt$ChannelScreen</ID>
<ID>ModifierMissing:ChannelSettingsItemList.kt$ChannelSelection</ID>
<ID>ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen</ID>
<ID>ModifierMissing:CommonCharts.kt$ChartHeader</ID>
<ID>ModifierMissing:CommonCharts.kt$Legend</ID>
<ID>ModifierMissing:CommonCharts.kt$TimeLabels</ID>
<ID>ModifierMissing:Connections.kt$ConnectionsScreen</ID>
<ID>ModifierMissing:ContactSharing.kt$SharedContactDialog</ID>
<ID>ModifierMissing:Contacts.kt$ContactListView</ID>
<ID>ModifierMissing:Contacts.kt$ContactsScreen</ID>
<ID>ModifierMissing:Contacts.kt$SelectionToolbar</ID>
<ID>ModifierMissing:DetectionSensorConfigItemList.kt$DetectionSensorConfigItemList</ID>
<ID>ModifierMissing:DeviceConfigItemList.kt$DeviceConfigItemList</ID>
<ID>ModifierMissing:DeviceMetrics.kt$DeviceMetricsScreen</ID>
<ID>ModifierMissing:DisplayConfigItemList.kt$DisplayConfigItemList</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPicker</ID>
<ID>ModifierMissing:EmojiPicker.kt$EmojiPickerDialog</ID>
<ID>ModifierMissing:EmptyStateContent.kt$EmptyStateContent</ID>
<ID>ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen</ID>
<ID>ModifierMissing:ExternalNotificationConfigItemList.kt$ExternalNotificationConfigItemList</ID>
<ID>ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen</ID>
<ID>ModifierMissing:IndoorAirQuality.kt$IndoorAirQuality</ID>
<ID>ModifierMissing:LoRaConfigItemList.kt$LoRaConfigItemList</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$LoraSignalIndicator</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Rssi</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$Snr</ID>
<ID>ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi</ID>
<ID>ModifierMissing:MQTTConfigItemList.kt$MQTTConfigItemList</ID>
<ID>ModifierMissing:Main.kt$MainScreen</ID>
<ID>ModifierMissing:MapReportingPreference.kt$MapReportingPreference</ID>
<ID>ModifierMissing:MessageActions.kt$MessageStatusButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReactionButton</ID>
<ID>ModifierMissing:MessageActions.kt$ReplyButton</ID>
<ID>ModifierMissing:NeighborInfoConfigItemList.kt$NeighborInfoConfigItemList</ID>
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigItemList</ID>
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
<ID>ModifierMissing:NetworkDevices.kt$NetworkDevices</ID>
<ID>ModifierMissing:NodeMenu.kt$NodeMenu</ID>
<ID>ModifierMissing:NodeScreen.kt$NodeScreen</ID>
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsItem</ID>
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsScreen</ID>
<ID>ModifierMissing:PaxcounterConfigItemList.kt$PaxcounterConfigItemList</ID>
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigItemList</ID>
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen</ID>
<ID>ModifierMissing:PositionLog.kt$PositionItem</ID>
<ID>ModifierMissing:PositionLog.kt$PositionLogScreen</ID>
<ID>ModifierMissing:PowerConfigItemList.kt$PowerConfigItemList</ID>
<ID>ModifierMissing:PowerMetrics.kt$PowerMetricsScreen</ID>
<ID>ModifierMissing:RadioConfig.kt$RadioConfigItemList</ID>
<ID>ModifierMissing:RangeTestConfigItemList.kt$RangeTestConfigItemList</ID>
<ID>ModifierMissing:RadioConfigScreenList.kt$RadioConfigScreenList</ID>
<ID>ModifierMissing:Reaction.kt$ReactionDialog</ID>
<ID>ModifierMissing:RemoteHardwareConfigItemList.kt$RemoteHardwareConfigItemList</ID>
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigItemList</ID>
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
<ID>ModifierMissing:SerialConfigItemList.kt$SerialConfigItemList</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItem</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemDetail</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemSwitch</ID>
@@ -342,20 +304,12 @@
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
<ID>ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog</ID>
<ID>ModifierMissing:SlidingSelector.kt$OptionLabel</ID>
<ID>ModifierMissing:StoreForwardConfigItemList.kt$StoreForwardConfigItemList</ID>
<ID>ModifierMissing:TelemetryConfigItemList.kt$TelemetryConfigItemList</ID>
<ID>ModifierMissing:TopLevelNavIcon.kt$TopLevelNavIcon</ID>
<ID>ModifierMissing:UserConfigItemList.kt$UserConfigItemList</ID>
<ID>ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End)</ID>
<ID>ModifierNotUsedAtRoot:BitwisePreference.kt$modifier = modifier.fillMaxWidth()</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:DeviceMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:DropDownPreference.kt$modifier = modifier .background( color = if (selectedItem == item.first) { MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) } else { Color.Unspecified }, )</ID>
<ID>ModifierNotUsedAtRoot:EditChannelDialog.kt$modifier = modifier.weight(1f)</ID>
<ID>ModifierNotUsedAtRoot:EditDeviceProfileDialog.kt$modifier = modifier.weight(1f)</ID>
<ID>ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.fillMaxWidth()</ID>
<ID>ModifierNotUsedAtRoot:EditListPreference.kt$modifier = modifier.padding(16.dp)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier = modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:EnvironmentCharts.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:NodeChip.kt$modifier = modifier.width(IntrinsicSize.Min).defaultMinSize(minWidth = 72.dp).semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } }</ID>
@@ -370,17 +324,9 @@
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .padding(all = 16.dp)</ID>
<ID>ModifierNotUsedAtRoot:TextDividerPreference.kt$modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End)</ID>
<ID>ModifierReused:BitwisePreference.kt$Checkbox( modifier = modifier .fillMaxWidth() .wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, )</ID>
<ID>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, ) } )</ID>
<ID>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 &lt; telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -&gt; 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), ) } }</ID>
<ID>ModifierReused:DeviceMetrics.kt$HorizontalLinesOverlay( modifier.width(dp), lineColors = listOf(graphColor, Color.Yellow, Color.Red, graphColor, graphColor), )</ID>
<ID>ModifierReused:DeviceMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())</ID>
<ID>ModifierReused:EditListPreference.kt$Column(modifier = modifier) { Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium) listState.forEachIndexed { index, value -&gt; 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&lt;Int&gt; 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&lt;ByteString&gt; 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&lt;RemoteHardwarePin&gt; 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 -&gt; 0 as T ByteString::class -&gt; ByteString.EMPTY as T RemoteHardwarePin::class -&gt; remoteHardwarePin {} as T else -&gt; throw IllegalArgumentException("Unsupported type: ${T::class}") } listState.add(listState.size, newElement) }, enabled = maxCount &gt; listState.size, ) { Text(text = stringResource(R.string.add)) } }</ID>
<ID>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, )</ID>
<ID>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, )</ID>
<ID>ModifierReused:EditListPreference.kt$Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium)</ID>
<ID>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) ) }</ID>
<ID>ModifierReused:EditTextPreference.kt$TextField( value = value, singleLine = true, modifier = modifier .fillMaxWidth() .onFocusEvent { isFocused = it.isFocused; onFocusChanged(it) }, enabled = enabled, isError = isError, onValueChange = { if (maxSize &gt; 0) { if (it.toByteArray().size &lt;= 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 ) } }, )</ID>
<ID>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, ) }</ID>
<ID>ModifierReused:EnvironmentCharts.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })</ID>
<ID>ModifierReused:EnvironmentCharts.kt$MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, )</ID>
@@ -408,18 +354,10 @@
<ID>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), ) } }</ID>
<ID>ModifierWithoutDefault:CommonCharts.kt$modifier</ID>
<ID>ModifierWithoutDefault:EnvironmentCharts.kt$modifier</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -&gt; "ShortTurbo" ModemPreset.SHORT_FAST -&gt; "ShortFast" ModemPreset.SHORT_SLOW -&gt; "ShortSlow" ModemPreset.MEDIUM_FAST -&gt; "MediumFast" ModemPreset.MEDIUM_SLOW -&gt; "MediumSlow" ModemPreset.LONG_FAST -&gt; "LongFast" ModemPreset.LONG_SLOW -&gt; "LongSlow" ModemPreset.LONG_MODERATE -&gt; "LongMod" ModemPreset.VERY_LONG_SLOW -&gt; "VLongSlow" else -&gt; "Invalid" }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toDoubleOrNull()?.let { double -&gt; valueState = it onValueChanged(double) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toFloatOrNull()?.let { float -&gt; valueState = it onValueChanged(float) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toUIntOrNull()?.toInt()?.let { int -&gt; valueState = it onValueChanged(int) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$onValueChanged(it)</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$valueState = it</ID>
<ID>MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex)</ID>
<ID>MultipleEmitters:CleanNodeDatabaseScreen.kt$NodesDeletionPreview</ID>
<ID>MultipleEmitters:CommonCharts.kt$LegendLabel</ID>
<ID>MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart</ID>
<ID>MultipleEmitters:EditTextPreference.kt$EditTextPreference</ID>
<ID>MultipleEmitters:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
<ID>MultipleEmitters:NodeDetail.kt$EncryptionErrorContent</ID>
<ID>MultipleEmitters:NodeDetail.kt$MetricsSection</ID>
@@ -430,7 +368,6 @@
<ID>MultipleEmitters:SignalMetrics.kt$SignalMetricsChart</ID>
<ID>MutableStateAutoboxing:Contacts.kt$mutableStateOf(2)</ID>
<ID>MutableStateParam:MessageList.kt$selectedIds</ID>
<ID>NestedBlockDepth:LanguageUtils.kt$LanguageUtils$fun getLanguageTags(context: Context): Map&lt;String, String&gt;</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
@@ -472,54 +409,32 @@
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>ParameterNaming:AmbientLightingConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:AudioConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:BitwisePreference.kt$onItemSelected</ID>
<ID>ParameterNaming:BluetoothConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:CannedMessageConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:ChannelSettingsItemList.kt$onPositiveClicked</ID>
<ID>ParameterNaming:ChannelSettingsItemList.kt$onSelected</ID>
<ID>ParameterNaming:CleanNodeDatabaseScreen.kt$onCheckedChanged</ID>
<ID>ParameterNaming:CleanNodeDatabaseScreen.kt$onDaysChanged</ID>
<ID>ParameterNaming:Contacts.kt$onDeleteSelected</ID>
<ID>ParameterNaming:Contacts.kt$onMuteSelected</ID>
<ID>ParameterNaming:DetectionSensorConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:DeviceConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:DisplayConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:DropDownPreference.kt$onItemSelected</ID>
<ID>ParameterNaming:EditIPv4Preference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditListPreference.kt$onValuesChanged</ID>
<ID>ParameterNaming:EditPasswordPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:EditTextPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:ExternalNotificationConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:LoRaConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:MQTTConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:MQTTConfigItemList.kt$onShouldReportLocationChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onMapReportingEnabledChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPositionPrecisionChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged</ID>
<ID>ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged</ID>
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
<ID>ParameterNaming:NeighborInfoConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:NetworkConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:NodeDetail.kt$onFirmwareSelected</ID>
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
<ID>ParameterNaming:PaxcounterConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:PositionConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:PositionPrecisionPreference.kt$onValueChanged</ID>
<ID>ParameterNaming:PowerConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onCancelClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onNegativeClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onPositiveClicked</ID>
<ID>ParameterNaming:PreferenceFooter.kt$onSaveClicked</ID>
<ID>ParameterNaming:RangeTestConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:RemoteHardwareConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:SerialConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:SlidingSelector.kt$onOptionSelected</ID>
<ID>ParameterNaming:StoreForwardConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:TelemetryConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:UsbDevices.kt$onDeviceSelected</ID>
<ID>ParameterNaming:UserConfigItemList.kt$onSaveClicked</ID>
<ID>ParameterNaming:WelcomeScreen.kt$onGetStarted</ID>
<ID>PreviewAnnotationNaming:LargeFontPreview.kt$LargeFontPreview$LargeFontPreview</ID>
<ID>PreviewPublic:BatteryInfo.kt$BatteryInfoPreview</ID>
@@ -596,19 +511,18 @@
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging</ID>
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$onBack: () -&gt; Unit</ID>
<ID>UnusedParameter:ChannelSettingsItemList.kt$title: String</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
<ID>ViewModelForwarding:Main.kt$MainAppBar( viewModel = uIViewModel, navController = navController, onAction = { action -&gt; when (action) { is NodeMenuAction.MoreDetails -&gt; { navController.navigate( NodesRoutes.NodeDetailGraph(action.node.num), { launchSingleTop = true restoreState = true }, ) } is NodeMenuAction.Share -&gt; sharedContact = action.node else -&gt; {} } }, )</ID>
<ID>ViewModelForwarding:Main.kt$NavGraph( modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(), uIViewModel = uIViewModel, bluetoothViewModel = bluetoothViewModel, navController = navController, )</ID>
<ID>ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)</ID>
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
<ID>ViewModelForwarding:Message.kt$MessageList( modifier = Modifier.fillMaxSize(), listState = listState, messages = messages, selectedIds = selectedMessageIds, onUnreadChanged = { messageId -&gt; onEvent(MessageScreenEvent.ClearUnreadCount(messageId)) }, onSendReaction = { emoji, id -&gt; onEvent(MessageScreenEvent.SendReaction(emoji, id)) }, viewModel = viewModel, contactKey = contactKey, onReply = { message -&gt; replyingToPacketId = message?.packetId }, onNodeMenuAction = { action -&gt; onEvent(MessageScreenEvent.HandleNodeMenuAction(action)) }, )</ID>
<ID>ViewModelForwarding:NodeDetail.kt$NodeDetailContent( node = node, ourNode = ourNode, metricsState = state, lastTracerouteTime = lastTracerouteTime, availableLogs = availableLogs, uiViewModel = uiViewModel, onAction = { action -&gt; handleNodeAction( action = action, uiViewModel = uiViewModel, node = node, navigateToMessages = navigateToMessages, onNavigateUp = onNavigateUp, onNavigate = onNavigate, viewModel = viewModel, ) }, modifier = modifier, )</ID>
<ID>ViewModelForwarding:NodeScreen.kt$AddContactFAB( modifier = Modifier.animateFloatingActionButton( visible = !isScrollInProgress &amp;&amp; connectionState == ConnectionState.CONNECTED &amp;&amp; shareCapable, alignment = Alignment.BottomEnd, ), model = model, onSharedContactImport = { contact -&gt; model.addSharedContact(contact) }, )</ID>
<ID>ViewModelInjection:DebugSearch.kt$viewModel</ID>
<ID>WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>Wrapping:DebugFilters.kt$(</ID>
<ID>Wrapping:DebugFilters.kt$if (filter in filterTexts) { Icon( imageVector = Icons.Filled.Done, contentDescription = stringResource(id = R.string.debug_filter_included), ) }</ID>
<ID>Wrapping:EditTextPreference.kt$;</ID>
<ID>Wrapping:MQTTRepository.kt$MQTTRepository.&lt;no name provided&gt;$(</ID>
<ID>Wrapping:Message.kt${ event -&gt; when (event) { is MessageScreenEvent.SendMessage -&gt; { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -&gt; viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -&gt; { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -&gt; viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.HandleNodeMenuAction -&gt; { when (val action = event.action) { is NodeMenuAction.DirectMessage -&gt; { val hasPKC = ourNode?.hasPKC == true &amp;&amp; action.node.hasPKC val targetChannel = if (hasPKC) { DataPacket.PKC_CHANNEL_INDEX } else { action.node.channel } navigateToMessages("$targetChannel${action.node.user.id}") } is NodeMenuAction.MoreDetails -&gt; navigateToNodeDetails(action.node.num) is NodeMenuAction.Share -&gt; sharedContact = action.node else -&gt; viewModel.handleNodeMenuAction(action) } } is MessageScreenEvent.SetTitle -&gt; viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -&gt; navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -&gt; navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -&gt; onNavigateBack() is MessageScreenEvent.CopyToClipboard -&gt; { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
<ID>Wrapping:SerialConnectionImpl.kt$SerialConnectionImpl$(</ID>

View File

@@ -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 <reified R : Route> NavGraphBuilder.addRadioConfigScreenComposable(
navController: NavHostController,
routeNameString: String,
crossinline screenContent: @Composable (viewModel: RadioConfigViewModel) -> Unit,
crossinline screenContent: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit,
) {
composable<R>(
deepLinks =
@@ -167,7 +168,7 @@ private inline fun <reified R : Route> NavGraphBuilder.addRadioConfigScreenCompo
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
val viewModel = hiltViewModel<RadioConfigViewModel>(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) },
),
;

View File

@@ -133,7 +133,6 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
companion object {
fun NavDestination.isTopLevel(): Boolean = listOf<KClass<out Route>>(
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,

View File

@@ -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 = {},

View File

@@ -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 = {},
)
}

View File

@@ -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 = {})
}

View File

@@ -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 = {})
}

View File

@@ -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 = { _, _ -> },
)
}

View File

@@ -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<ChannelSettings>,
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 {

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<T : MessageLite>(private val initialValue: T) {
var value by mutableStateOf(initialValue)
val isDirty: Boolean
get() = value != initialValue
fun reset() {
value = initialValue
}
companion object {
fun <T : MessageLite> saver(initialValue: T): Saver<ConfigState<T>, 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 <T : MessageLite> rememberConfigState(initialValue: T): ConfigState<T> =
rememberSaveable(initialValue, saver = ConfigState.saver(initialValue)) { ConfigState(initialValue) }

View File

@@ -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 = {},
)
}

View File

@@ -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 = {})
}

View File

@@ -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 = {})
}

View File

@@ -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 = { _, _ -> },
)
}

View File

@@ -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 = {},
)
}

View File

@@ -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 = {},
)
}

View File

@@ -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 = {},
)
}

View File

@@ -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)

View File

@@ -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 = {},
)
}

View File

@@ -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 = {},
)
}

View File

@@ -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 = {})
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T : MessageLite> RadioConfigScreenList(
title: String,
onBack: () -> Unit,
responseState: ResponseState<Any>,
onDismissPacketResponse: () -> Unit,
configState: ConfigState<T>,
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)
},
)
}
}
}
}

View File

@@ -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 = {})
}

View File

@@ -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 = {},
)
}

View File

@@ -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 = {})
}

View File

@@ -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 = {})
}

View File

@@ -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 = {},
)
}

View File

@@ -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 = {})
}

View File

@@ -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" },
)
}

View File

@@ -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",

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" ?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CommentSpacing:NodeInfo.kt$NodeInfo$/// @return a nice human readable string for the distance, or null for unknown</ID>
<ID>CommentSpacing:NodeInfo.kt$NodeInfo$/// @return bearing to the other position in degrees</ID>
<ID>CommentSpacing:NodeInfo.kt$NodeInfo$/// @return distance in meters to some other node (or null if unknown)</ID>
<ID>CommentSpacing:NodeInfo.kt$NodeInfo$/// return the position if it is valid, else null</ID>
<ID>CommentSpacing:NodeInfo.kt$Position$/// @return bearing to the other position in degrees</ID>
<ID>CommentSpacing:NodeInfo.kt$Position$/// @return distance in meters to some other node (or null if unknown)</ID>
<ID>CommentSpacing:NodeInfo.kt$Position.Companion$/// Convert to a double representation of degrees</ID>
<ID>FinalNewline:DataPacket.kt$com.geeksville.mesh.DataPacket.kt</ID>
<ID>FinalNewline:MyNodeInfo.kt$com.geeksville.mesh.MyNodeInfo.kt</ID>
<ID>FinalNewline:NodeInfo.kt$com.geeksville.mesh.NodeInfo.kt</ID>
<ID>FunctionParameterNaming:LocationUtils.kt$_degIn: Double</ID>
<ID>FunctionParameterNaming:LocationUtils.kt$lat_a: Double</ID>
<ID>FunctionParameterNaming:LocationUtils.kt$lat_b: Double</ID>
<ID>FunctionParameterNaming:LocationUtils.kt$lng_a: Double</ID>
<ID>FunctionParameterNaming:LocationUtils.kt$lng_b: Double</ID>
<ID>ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format( "%s%s %.6s %.7s", UTM.zone, UTM.toMGRS().band, UTM.easting, UTM.northing )</ID>
<ID>ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format( "%s%s %s%s %05d %05d", MGRS.zone, MGRS.band, MGRS.column, MGRS.row, MGRS.easting, MGRS.northing )</ID>
<ID>ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format("%.5f %.5f", p.latitude, p.longitude)</ID>
<ID>ImplicitDefaultLocale:LocationUtils.kt$GPSFormat$String.format("%s°%s'%.5s\"%s", a[0], a[1], a[2], a[3])</ID>
<ID>ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel)</ID>
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
<ID>MagicNumber:Extensions.kt$1000</ID>
<ID>MagicNumber:Extensions.kt$1440000</ID>
<ID>MagicNumber:Extensions.kt$24</ID>
<ID>MagicNumber:Extensions.kt$2880</ID>
<ID>MagicNumber:Extensions.kt$60</ID>
<ID>MagicNumber:LocationUtils.kt$0.8</ID>
<ID>MagicNumber:LocationUtils.kt$110540</ID>
<ID>MagicNumber:LocationUtils.kt$111320</ID>
<ID>MagicNumber:LocationUtils.kt$180</ID>
<ID>MagicNumber:LocationUtils.kt$1e-7</ID>
<ID>MagicNumber:LocationUtils.kt$360</ID>
<ID>MagicNumber:LocationUtils.kt$360.0</ID>
<ID>MagicNumber:LocationUtils.kt$3600.0</ID>
<ID>MagicNumber:LocationUtils.kt$60</ID>
<ID>MagicNumber:LocationUtils.kt$60.0</ID>
<ID>MagicNumber:LocationUtils.kt$6366000</ID>
<ID>MagicNumber:LocationUtils.kt$GPSFormat$3</ID>
<ID>MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.114</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.299</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.587</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000.0</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$15</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$16</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609.34</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$255</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$3.281</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$60</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$8</ID>
<ID>MagicNumber:NodeInfo.kt$Position$180</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90.0</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e-7</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e7</ID>
<ID>MatchingDeclarationName:LocationUtils.kt$GPSFormat</ID>
<ID>MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int</ID>
<ID>MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE &amp;&amp; dist &lt; 1609 -&gt; "%.0f ft".format(dist.toDouble()*3.281)</ID>
<ID>MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE &amp;&amp; dist &gt;= 1609 -&gt; "%.1f mi".format(dist / 1609.34)</ID>
<ID>MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE &amp;&amp; dist &lt; 1000 -&gt; "%.0f m".format(dist.toDouble())</ID>
<ID>MaxLineLength:NodeInfo.kt$NodeInfo$prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE &amp;&amp; dist &gt;= 1000 -&gt; "%.1f km".format(dist / 1000.0)</ID>
<ID>MaxLineLength:NodeInfo.kt$Position$/**</ID>
<ID>MaxLineLength:NodeInfo.kt$Position$return "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=${time})"</ID>
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase()</ID>
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$null</ID>
<ID>NewLineAtEndOfFile:DataPacket.kt$com.geeksville.mesh.DataPacket.kt</ID>
<ID>NewLineAtEndOfFile:MyNodeInfo.kt$com.geeksville.mesh.MyNodeInfo.kt</ID>
<ID>NewLineAtEndOfFile:NodeInfo.kt$com.geeksville.mesh.NodeInfo.kt</ID>
<ID>NoConsecutiveBlankLines:NodeInfo.kt$ </ID>
<ID>SpacingAroundOperators:NodeInfo.kt$NodeInfo$*</ID>
<ID>StringTemplate:NodeInfo.kt$Position$${time}</ID>
<ID>TooManyFunctions:LocationUtils.kt$com.geeksville.mesh.util.LocationUtils.kt</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -1,378 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues>
<ID>TooManyFunctions:ContactSharing.kt$com.geeksville.mesh.ui.ContactSharing.kt</ID>
<ID>TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.NodeDetail.kt</ID>
</ManuallySuppressedIssues>
<CurrentIssues>
<ID>ChainWrapping:Channel.kt$Channel$&amp;&amp;</ID>
<ID>CommentSpacing:BLEException.kt$BLEConnectionClosing$/// Our interface is being shut down</ID>
<ID>CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back</ID>
<ID>CommentSpacing:ContextExtensions.kt$/// Utility function to hide the soft keyboard per stack overflow</ID>
<ID>CommentSpacing:ContextExtensions.kt$/// show a toast</ID>
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// Queue some new work</ID>
<ID>CommentSpacing:DeferredExecution.kt$DeferredExecution$/// run all work in the queue and clear it to be ready to accept new work</ID>
<ID>CommentSpacing:Exceptions.kt$/// Convert any exceptions in this service call into a RemoteException that the client can</ID>
<ID>CommentSpacing:Exceptions.kt$/// then handle</ID>
<ID>CommentSpacing:Exceptions.kt$Exceptions$/// Set in Application.onCreate</ID>
<ID>CommentSpacing:MockInterface.kt$MockInterface$/// Generate a fake node info entry</ID>
<ID>CommentSpacing:MockInterface.kt$MockInterface$/// Generate a fake text message from a node</ID>
<ID>CommentSpacing:MockInterface.kt$MockInterface$/// Send a fake ack packet back if the sender asked for want_ack</ID>
<ID>ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "message") val raw_message: String</ID>
<ID>ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "received_date") val received_date: Long</ID>
<ID>ConstructorParameterNaming:MeshLog.kt$MeshLog$@ColumnInfo(name = "type") val message_type: String</ID>
<ID>ConstructorParameterNaming:Packet.kt$ContactSettings$@PrimaryKey val contact_key: String</ID>
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String</ID>
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int</ID>
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID>
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>EmptyCatchBlock:MeshLog.kt$MeshLog${ }</ID>
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
<ID>EmptyFunctionBlock:NsdManager.kt$&lt;no name provided&gt;${ }</ID>
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
<ID>FinalNewline:AppPrefs.kt$com.geeksville.mesh.android.AppPrefs.kt</ID>
<ID>FinalNewline:ApplicationModule.kt$com.geeksville.mesh.ApplicationModule.kt</ID>
<ID>FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
<ID>FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
<ID>FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
<ID>FinalNewline:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt</ID>
<ID>FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>FinalNewline:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt</ID>
<ID>FinalNewline:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt</ID>
<ID>FinalNewline:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>FinalNewline:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>FinalNewline:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>FinalNewline:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>FinalNewline:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>FinalNewline:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt</ID>
<ID>FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>FinalNewline:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt</ID>
<ID>FinalNewline:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt</ID>
<ID>FinalNewline:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>FinalNewline:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>FinalNewline:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt</ID>
<ID>FinalNewline:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:SafeBluetooth.kt$SafeBluetooth$// TODO: display some kind of UI about restarting BLE</ID>
<ID>FunctionNaming:PacketDao.kt$PacketDao$@Query("DELETE FROM packet WHERE uuid=:uuid") suspend fun _delete(uuid: Long)</ID>
<ID>FunctionNaming:QuickChatActionDao.kt$QuickChatActionDao$@Query("Delete from quick_chat where uuid=:uuid") fun _delete(uuid: Long)</ID>
<ID>ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel)</ID>
<ID>LargeClass:MeshService.kt$MeshService : ServiceLogging</ID>
<ID>LongMethod:AmbientLightingConfigItemList.kt$@Composable fun AmbientLightingConfigItemList( ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig, enabled: Boolean, onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -&gt; Unit, )</ID>
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigItemList( audioConfig: AudioConfig, enabled: Boolean, onSaveClicked: (AudioConfig) -&gt; Unit, )</ID>
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigItemList( messages: String, cannedMessageConfig: CannedMessageConfig, enabled: Boolean, onSaveClicked: (messages: String, config: CannedMessageConfig) -&gt; Unit, )</ID>
<ID>LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigItemList( deviceConfig: DeviceConfig, enabled: Boolean, onSaveClicked: (DeviceConfig) -&gt; Unit, )</ID>
<ID>LongMethod:DropDownPreference.kt$@Composable fun &lt;T&gt; DropDownPreference( title: String, enabled: Boolean, items: List&lt;Pair&lt;T, String&gt;&gt;, selectedItem: T, onItemSelected: (T) -&gt; Unit, modifier: Modifier = Modifier, summary: String? = null, )</ID>
<ID>LongMethod:EditListPreference.kt$@Composable inline fun &lt;reified T&gt; EditListPreference( title: String, list: List&lt;T&gt;, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List&lt;T&gt;) -&gt; Unit, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -&gt; Unit, )</ID>
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -&gt; Unit, )</ID>
<ID>LongMethod:StoreForwardConfigItemList.kt$@Composable fun StoreForwardConfigItemList( storeForwardConfig: StoreForwardConfig, enabled: Boolean, onSaveClicked: (StoreForwardConfig) -&gt; Unit, )</ID>
<ID>LongMethod:TelemetryConfigItemList.kt$@Composable fun TelemetryConfigItemList( telemetryConfig: TelemetryConfig, enabled: Boolean, onSaveClicked: (TelemetryConfig) -&gt; Unit, )</ID>
<ID>LongParameterList:NOAAWmsTileSource.kt$NOAAWmsTileSource$( aName: String, aBaseUrl: Array&lt;String&gt;, layername: String, version: String, time: String?, srs: String, style: String?, format: String, )</ID>
<ID>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, )</ID>
<ID>MagicNumber:BatteryInfo.kt$100</ID>
<ID>MagicNumber:BatteryInfo.kt$101</ID>
<ID>MagicNumber:BatteryInfo.kt$14</ID>
<ID>MagicNumber:BatteryInfo.kt$15</ID>
<ID>MagicNumber:BatteryInfo.kt$34</ID>
<ID>MagicNumber:BatteryInfo.kt$35</ID>
<ID>MagicNumber:BatteryInfo.kt$4</ID>
<ID>MagicNumber:BatteryInfo.kt$5</ID>
<ID>MagicNumber:BatteryInfo.kt$79</ID>
<ID>MagicNumber:BatteryInfo.kt$80</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$1000</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$500</ID>
<ID>MagicNumber:BluetoothInterface.kt$BluetoothInterface$512</ID>
<ID>MagicNumber:Channel.kt$0xff</ID>
<ID>MagicNumber:ChannelOption.kt$.03125f</ID>
<ID>MagicNumber:ChannelOption.kt$.0625f</ID>
<ID>MagicNumber:ChannelOption.kt$.203125f</ID>
<ID>MagicNumber:ChannelOption.kt$.40625f</ID>
<ID>MagicNumber:ChannelOption.kt$.8125f</ID>
<ID>MagicNumber:ChannelOption.kt$1.6250f</ID>
<ID>MagicNumber:ChannelOption.kt$1000f</ID>
<ID>MagicNumber:ChannelOption.kt$1600</ID>
<ID>MagicNumber:ChannelOption.kt$200</ID>
<ID>MagicNumber:ChannelOption.kt$3.25f</ID>
<ID>MagicNumber:ChannelOption.kt$31</ID>
<ID>MagicNumber:ChannelOption.kt$400</ID>
<ID>MagicNumber:ChannelOption.kt$5</ID>
<ID>MagicNumber:ChannelOption.kt$62</ID>
<ID>MagicNumber:ChannelOption.kt$800</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f</ID>
<ID>MagicNumber:ChannelSet.kt$40</ID>
<ID>MagicNumber:ChannelSet.kt$960</ID>
<ID>MagicNumber:Contacts.kt$7</ID>
<ID>MagicNumber:Contacts.kt$8</ID>
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
<ID>MagicNumber:Debug.kt$3</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID>
<ID>MagicNumber:DownloadButton.kt$1.25f</ID>
<ID>MagicNumber:EditChannelDialog.kt$16</ID>
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
<ID>MagicNumber:EditIPv4Preference.kt$0xff</ID>
<ID>MagicNumber:EditIPv4Preference.kt$16</ID>
<ID>MagicNumber:EditIPv4Preference.kt$24</ID>
<ID>MagicNumber:EditIPv4Preference.kt$8</ID>
<ID>MagicNumber:EditListPreference.kt$12</ID>
<ID>MagicNumber:EditListPreference.kt$12345</ID>
<ID>MagicNumber:EditListPreference.kt$67890</ID>
<ID>MagicNumber:Extensions.kt$1000</ID>
<ID>MagicNumber:Extensions.kt$1440000</ID>
<ID>MagicNumber:Extensions.kt$24</ID>
<ID>MagicNumber:Extensions.kt$2880</ID>
<ID>MagicNumber:Extensions.kt$60</ID>
<ID>MagicNumber:LazyColumnDragAndDropDemo.kt$50</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$1000L</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$30</ID>
<ID>MagicNumber:LocationRepository.kt$LocationRepository$31</ID>
<ID>MagicNumber:LocationUtils.kt$1e-7</ID>
<ID>MagicNumber:LocationUtils.kt$360</ID>
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
<ID>MagicNumber:MapView.kt$0.5f</ID>
<ID>MagicNumber:MapView.kt$1.3</ID>
<ID>MagicNumber:MapView.kt$1024.0</ID>
<ID>MagicNumber:MapView.kt$128205</ID>
<ID>MagicNumber:MapView.kt$12F</ID>
<ID>MagicNumber:MapView.kt$&lt;no name provided&gt;$1e7</ID>
<ID>MagicNumber:MapViewExtensions.kt$1e-5</ID>
<ID>MagicNumber:MapViewExtensions.kt$1e-7</ID>
<ID>MagicNumber:MapViewExtensions.kt$3.0f</ID>
<ID>MagicNumber:MapViewExtensions.kt$40f</ID>
<ID>MagicNumber:MapViewExtensions.kt$60f</ID>
<ID>MagicNumber:MapViewExtensions.kt$80f</ID>
<ID>MagicNumber:MarkerWithLabel.kt$MarkerWithLabel$3</ID>
<ID>MagicNumber:MeshService.kt$MeshService$0xffffffff</ID>
<ID>MagicNumber:MeshService.kt$MeshService$100</ID>
<ID>MagicNumber:MeshService.kt$MeshService$1000</ID>
<ID>MagicNumber:MeshService.kt$MeshService$1000.0</ID>
<ID>MagicNumber:MeshService.kt$MeshService$1000L</ID>
<ID>MagicNumber:MeshService.kt$MeshService$16</ID>
<ID>MagicNumber:MeshService.kt$MeshService$30</ID>
<ID>MagicNumber:MeshService.kt$MeshService$32</ID>
<ID>MagicNumber:MeshService.kt$MeshService$60000</ID>
<ID>MagicNumber:MeshService.kt$MeshService$8</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1000L</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-5</ID>
<ID>MagicNumber:MetricsViewModel.kt$MetricsViewModel$1e-7</ID>
<ID>MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$180</ID>
<ID>MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$256</ID>
<ID>MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$360.0</ID>
<ID>MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$4</ID>
<ID>MagicNumber:NOAAWmsTileSource.kt$NOAAWmsTileSource$5</ID>
<ID>MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.114</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.299</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.587</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000.0</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$16</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609.34</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$255</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$3.281</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$8</ID>
<ID>MagicNumber:NodeInfo.kt$Position$180</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90.0</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e-7</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e7</ID>
<ID>MagicNumber:PacketRepository.kt$PacketRepository$500</ID>
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$10</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$100</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$1000</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$2500</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth.&lt;no name provided&gt;$2500</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$0xff</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$3</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$4</ID>
<ID>MagicNumber:StreamInterface.kt$StreamInterface$8</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$1000</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$180</ID>
<ID>MagicNumber:TCPInterface.kt$TCPInterface$500</ID>
<ID>MagicNumber:UIState.kt$4</ID>
<ID>MatchingDeclarationName:AnalyticsClient.kt$AnalyticsProvider</ID>
<ID>MatchingDeclarationName:DistanceExtensions.kt$DistanceUnit</ID>
<ID>MatchingDeclarationName:LocationUtils.kt$GPSFormat</ID>
<ID>MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker</ID>
<ID>MatchingDeclarationName:SortOption.kt$NodeSortOption</ID>
<ID>MaxLineLength:AppPrefs.kt$FloatPref$fun get(thisRef: AppPrefs, prop: KProperty&lt;Float&gt;): Float</ID>
<ID>MaxLineLength:AppPrefs.kt$StringPref$fun get(thisRef: AppPrefs, prop: KProperty&lt;String&gt;): String</ID>
<ID>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 &gt;= 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) */</ID>
<ID>MaxLineLength:BluetoothState.kt$BluetoothState$"BluetoothState(hasPermissions=$hasPermissions, enabled=$enabled, bondedDevices=${bondedDevices.map { it.anonymize }})"</ID>
<ID>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</ID>
<ID>MaxLineLength:DataPacket.kt$DataPacket$val dataType: Int</ID>
<ID>MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq</ID>
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
<ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID>
<ID>MaxLineLength:ServiceClient.kt$ServiceClient$// Some phones seem to ahve a race where if you unbind and quickly rebind bindService returns false. Try</ID>
<ID>MaxLineLength:ServiceClient.kt$ServiceClient.&lt;no name provided&gt;$// If we start to close a service, it seems that there is a possibility a onServiceConnected event is the queue</ID>
<ID>MaxLineLength:StreamInterface.kt$StreamInterface$*</ID>
<ID>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)</ID>
<ID>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</ID>
<ID>MaxLineLength:StreamInterface.kt$StreamInterface$deliverPacket()</ID>
<ID>MaxLineLength:StreamInterface.kt$StreamInterface$lostSync()</ID>
<ID>MaxLineLength:StreamInterface.kt$StreamInterface$service.onDisconnect(isPermanent = true)</ID>
<ID>MayBeConst:AppPrefs.kt$AppPrefs.Companion$private val baseName = "appPrefs_"</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -&gt; "ShortTurbo" ModemPreset.SHORT_FAST -&gt; "ShortFast" ModemPreset.SHORT_SLOW -&gt; "ShortSlow" ModemPreset.MEDIUM_FAST -&gt; "MediumFast" ModemPreset.MEDIUM_SLOW -&gt; "MediumSlow" ModemPreset.LONG_FAST -&gt; "LongFast" ModemPreset.LONG_SLOW -&gt; "LongSlow" ModemPreset.LONG_MODERATE -&gt; "LongMod" ModemPreset.VERY_LONG_SLOW -&gt; "VLongSlow" else -&gt; "Invalid" }</ID>
<ID>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, )</ID>
<ID>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, )</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toDoubleOrNull()?.let { double -&gt; valueState = it onValueChanged(double) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toFloatOrNull()?.let { float -&gt; valueState = it onValueChanged(float) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$it.toUIntOrNull()?.toInt()?.let { int -&gt; valueState = it onValueChanged(int) }</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$onValueChanged(it)</ID>
<ID>MultiLineIfElse:EditTextPreference.kt$valueState = it</ID>
<ID>MultiLineIfElse:Exceptions.kt$Exceptions.errormsg("ignoring exception", ex)</ID>
<ID>NestedBlockDepth:LanguageUtils.kt$LanguageUtils$fun getLanguageTags(context: Context): Map&lt;String, String&gt;</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage)</ID>
<ID>NestedBlockDepth:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>NestedBlockDepth:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>NewLineAtEndOfFile:AppPrefs.kt$com.geeksville.mesh.android.AppPrefs.kt</ID>
<ID>NewLineAtEndOfFile:ApplicationModule.kt$com.geeksville.mesh.ApplicationModule.kt</ID>
<ID>NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
<ID>NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
<ID>NewLineAtEndOfFile:BootCompleteReceiver.kt$com.geeksville.mesh.service.BootCompleteReceiver.kt</ID>
<ID>NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
<ID>NewLineAtEndOfFile:DeferredExecution.kt$com.geeksville.mesh.concurrent.DeferredExecution.kt</ID>
<ID>NewLineAtEndOfFile:DeviceVersion.kt$com.geeksville.mesh.model.DeviceVersion.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceId.kt$com.geeksville.mesh.repository.radio.InterfaceId.kt</ID>
<ID>NewLineAtEndOfFile:InterfaceSpec.kt$com.geeksville.mesh.repository.radio.InterfaceSpec.kt</ID>
<ID>NewLineAtEndOfFile:MockInterfaceFactory.kt$com.geeksville.mesh.repository.radio.MockInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:NopInterface.kt$com.geeksville.mesh.repository.radio.NopInterface.kt</ID>
<ID>NewLineAtEndOfFile:NopInterfaceFactory.kt$com.geeksville.mesh.repository.radio.NopInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>NewLineAtEndOfFile:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt</ID>
<ID>NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>NewLineAtEndOfFile:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt</ID>
<ID>NewLineAtEndOfFile:RegularPreference.kt$com.geeksville.mesh.ui.common.components.RegularPreference.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnection.kt$com.geeksville.mesh.repository.usb.SerialConnection.kt</ID>
<ID>NewLineAtEndOfFile:SerialConnectionListener.kt$com.geeksville.mesh.repository.usb.SerialConnectionListener.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterface.kt$com.geeksville.mesh.repository.radio.SerialInterface.kt</ID>
<ID>NewLineAtEndOfFile:SerialInterfaceFactory.kt$com.geeksville.mesh.repository.radio.SerialInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>NoBlankLineBeforeRbrace:DebugLogFile.kt$BinaryLogFile$ </ID>
<ID>NoBlankLineBeforeRbrace:NopInterface.kt$NopInterface$ </ID>
<ID>NoConsecutiveBlankLines:AppPrefs.kt$ </ID>
<ID>NoConsecutiveBlankLines:BootCompleteReceiver.kt$ </ID>
<ID>NoConsecutiveBlankLines:Constants.kt$ </ID>
<ID>NoConsecutiveBlankLines:DebugLogFile.kt$ </ID>
<ID>NoConsecutiveBlankLines:DeferredExecution.kt$ </ID>
<ID>NoConsecutiveBlankLines:Exceptions.kt$ </ID>
<ID>NoConsecutiveBlankLines:IRadioInterface.kt$ </ID>
<ID>NoEmptyClassBody:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>NoSemicolons:DateUtils.kt$DateUtils$;</ID>
<ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>NoWildcardImports:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
<ID>OptionalAbstractKeyword:SyncContinuation.kt$Continuation$abstract</ID>
<ID>ParameterListWrapping:AppPrefs.kt$FloatPref$(thisRef: AppPrefs, prop: KProperty&lt;Float&gt;)</ID>
<ID>ParameterListWrapping:AppPrefs.kt$StringPref$(thisRef: AppPrefs, prop: KProperty&lt;String&gt;)</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>SpacingAroundCurly:AppPrefs.kt$FloatPref$}</ID>
<ID>SpacingAroundKeyword:AppPrefs.kt$AppPrefs$if</ID>
<ID>SpacingAroundKeyword:Exceptions.kt$if</ID>
<ID>SpacingAroundKeyword:Exceptions.kt$when</ID>
<ID>SpacingAroundRangeOperator:BatteryInfo.kt$..</ID>
<ID>SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException</ID>
<ID>SwallowedException:ChannelSet.kt$ex: Throwable</ID>
<ID>SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>SwallowedException:MeshLog.kt$MeshLog$e: IOException</ID>
<ID>SwallowedException:MeshService.kt$MeshService$e: Exception</ID>
<ID>SwallowedException:MeshService.kt$MeshService$e: TimeoutException</ID>
<ID>SwallowedException:MeshService.kt$MeshService$ex: BLEException</ID>
<ID>SwallowedException:MeshService.kt$MeshService$ex: CancellationException</ID>
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
<ID>SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: DeadObjectException</ID>
<ID>SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException</ID>
<ID>SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException</ID>
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
<ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception</ID>
<ID>TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:LanguageUtils.kt$LanguageUtils$e: Exception</ID>
<ID>TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception</ID>
<ID>TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MainActivity.kt$MainActivity$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MapView.kt$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MapViewModel.kt$MapViewModel$e: Exception</ID>
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$e: Exception</ID>
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService.&lt;no name provided&gt;$ex: Exception</ID>
<ID>TooGenericExceptionCaught:MeshServiceStarter.kt$ServiceStarter$ex: Exception</ID>
<ID>TooGenericExceptionCaught:RadioConfigViewModel.kt$RadioConfigViewModel$ex: Exception</ID>
<ID>TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: Exception</ID>
<ID>TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException</ID>
<ID>TooGenericExceptionCaught:SqlTileWriterExt.kt$SqlTileWriterExt$e: Exception</ID>
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
<ID>TooGenericExceptionThrown:DeviceVersion.kt$DeviceVersion$throw Exception("Can't parse version $s")</ID>
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService$throw Exception("Can't set user without a NodeInfo")</ID>
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService.&lt;no name provided&gt;$throw Exception("Port numbers must be non-zero!")</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Haven't called connect")</ID>
<ID>TooGenericExceptionThrown:ServiceClient.kt$ServiceClient$throw Exception("Service not bound")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("SyncContinuation timeout")</ID>
<ID>TooGenericExceptionThrown:SyncContinuation.kt$SyncContinuation$throw Exception("This shouldn't happen")</ID>
<ID>TooManyFunctions:AppPrefs.kt$AppPrefs</ID>
<ID>TooManyFunctions:BluetoothInterface.kt$BluetoothInterface : IRadioInterfaceLogging</ID>
<ID>TooManyFunctions:MainActivity.kt$MainActivity : AppCompatActivityLogging</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService : ServiceLogging</ID>
<ID>TooManyFunctions:MeshService.kt$MeshService$&lt;no name provided&gt; : Stub</ID>
<ID>TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt</ID>
<ID>TooManyFunctions:PacketDao.kt$PacketDao</ID>
<ID>TooManyFunctions:PacketRepository.kt$PacketRepository</ID>
<ID>TooManyFunctions:RadioConfigRepository.kt$RadioConfigRepository</ID>
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModelLogging</ID>
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService : Logging</ID>
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : LoggingCloseable</ID>
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModelLogging</ID>
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
<ID>UnusedPrivateMember:NOAAWmsTileSource.kt$NOAAWmsTileSource$private fun tile2lat(y: Int, z: Int): Double</ID>
<ID>UnusedPrivateMember:NOAAWmsTileSource.kt$NOAAWmsTileSource$private fun tile2lon(x: Int, z: Int): Double</ID>
<ID>UtilityClassWithPublicConstructor:CustomTileSource.kt$CustomTileSource</ID>
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
<ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>WildcardImport:UsbRepository.kt$import kotlinx.coroutines.flow.*</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Channel.kt$0xff</ID>
<ID>MagicNumber:ChannelOption.kt$.03125f</ID>
<ID>MagicNumber:ChannelOption.kt$.0625f</ID>
<ID>MagicNumber:ChannelOption.kt$.203125f</ID>
<ID>MagicNumber:ChannelOption.kt$.40625f</ID>
<ID>MagicNumber:ChannelOption.kt$.8125f</ID>
<ID>MagicNumber:ChannelOption.kt$1.6250f</ID>
<ID>MagicNumber:ChannelOption.kt$1000f</ID>
<ID>MagicNumber:ChannelOption.kt$1600</ID>
<ID>MagicNumber:ChannelOption.kt$200</ID>
<ID>MagicNumber:ChannelOption.kt$3.25f</ID>
<ID>MagicNumber:ChannelOption.kt$31</ID>
<ID>MagicNumber:ChannelOption.kt$400</ID>
<ID>MagicNumber:ChannelOption.kt$5</ID>
<ID>MagicNumber:ChannelOption.kt$62</ID>
<ID>MagicNumber:ChannelOption.kt$800</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_MODERATE$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.LONG_SLOW$.125f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.MEDIUM_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_FAST$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.SHORT_SLOW$.250f</ID>
<ID>MagicNumber:ChannelOption.kt$ChannelOption.VERY_LONG_SLOW$.0625f</ID>
</CurrentIssues>
</SmellBaseline>

View File

@@ -898,4 +898,5 @@
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
<string name="device_configuration">Device configuration</string>
<string name="remotely_administrating">"[Remote] %1$s"</string>
</resources>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ImplicitDefaultLocale:NodeInfo.kt$NodeInfo$String.format("%d%%", batteryLevel)</ID>
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
<ID>MagicNumber:Extensions.kt$1000</ID>
<ID>MagicNumber:Extensions.kt$1440000</ID>
<ID>MagicNumber:Extensions.kt$24</ID>
<ID>MagicNumber:Extensions.kt$2880</ID>
<ID>MagicNumber:Extensions.kt$60</ID>
<ID>MagicNumber:LocationUtils.kt$1e-7</ID>
<ID>MagicNumber:NodeInfo.kt$DeviceMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$EnvironmentMetrics.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.114</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.299</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0.587</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x0000FF</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0x00FF00</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$0xFF0000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1000.0</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$16</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$1609.34</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$255</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$3.281</ID>
<ID>MagicNumber:NodeInfo.kt$NodeInfo$8</ID>
<ID>MagicNumber:NodeInfo.kt$Position$180</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90</ID>
<ID>MagicNumber:NodeInfo.kt$Position$90.0</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e-7</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e7</ID>
<ID>MatchingDeclarationName:LocationUtils.kt$GPSFormat</ID>
</CurrentIssues>
</SmellBaseline>