mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Add :feature:node (#3275)
This commit is contained in:
parent
5a6cd5acbc
commit
d553cdfee6
44 changed files with 164 additions and 178 deletions
|
|
@ -197,6 +197,7 @@ dependencies {
|
|||
implementation(projects.core.strings)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.feature.map)
|
||||
implementation(projects.feature.node)
|
||||
|
||||
// Bundles
|
||||
implementation(libs.bundles.markdown)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" ?>
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<ID>CommentSpacing:Constants.kt$/// a bool true means we expect this condition to continue until, false means device might come back</ID>
|
||||
<ID>CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib</ID>
|
||||
<ID>CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */</ID>
|
||||
<ID>ComposableNaming:NodeDetail.kt$notesSection</ID>
|
||||
<ID>ComposableNaming:NodeDetailScreen.kt$notesSection</ID>
|
||||
<ID>ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList</ID>
|
||||
<ID>ComposableParamOrder:Debug.kt$DebugMenuActions</ID>
|
||||
<ID>ComposableParamOrder:Debug.kt$DecodedPayloadBlock</ID>
|
||||
|
|
@ -14,15 +14,12 @@
|
|||
<ID>ComposableParamOrder:DebugSearch.kt$DebugSearchStateviewModelDefaults</ID>
|
||||
<ID>ComposableParamOrder:DeviceMetrics.kt$DeviceMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:EditBase64Preference.kt$EditBase64Preference</ID>
|
||||
<ID>ComposableParamOrder:ElevationInfo.kt$ElevationInfo</ID>
|
||||
<ID>ComposableParamOrder:EmptyStateContent.kt$EmptyStateContent</ID>
|
||||
<ID>ComposableParamOrder:EnvironmentCharts.kt$ChartContent</ID>
|
||||
<ID>ComposableParamOrder:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:EnvironmentCharts.kt$MetricPlottingCanvas</ID>
|
||||
<ID>ComposableParamOrder:HostMetricsLog.kt$HostMetricsItem</ID>
|
||||
<ID>ComposableParamOrder:HostMetricsLog.kt$LogLine</ID>
|
||||
<ID>ComposableParamOrder:LastHeardInfo.kt$LastHeardInfo</ID>
|
||||
<ID>ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates</ID>
|
||||
<ID>ComposableParamOrder:MainAppBar.kt$MainAppBar</ID>
|
||||
<ID>ComposableParamOrder:MapReportingPreference.kt$MapReportingPreference</ID>
|
||||
<ID>ComposableParamOrder:Message.kt$MessageScreen</ID>
|
||||
|
|
@ -32,24 +29,16 @@
|
|||
<ID>ComposableParamOrder:MessageItem.kt$MessageItem</ID>
|
||||
<ID>ComposableParamOrder:MessageList.kt$DeliveryInfo</ID>
|
||||
<ID>ComposableParamOrder:MessageList.kt$MessageList</ID>
|
||||
<ID>ComposableParamOrder:NodeChip.kt$NodeChip</ID>
|
||||
<ID>ComposableParamOrder:NodeDetail.kt$DeviceActions</ID>
|
||||
<ID>ComposableParamOrder:NodeDetail.kt$EnvironmentMetrics</ID>
|
||||
<ID>ComposableParamOrder:NodeDetail.kt$NodeActionButton</ID>
|
||||
<ID>ComposableParamOrder:NodeDetail.kt$NodeDetailList</ID>
|
||||
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
|
||||
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
|
||||
<ID>ComposableParamOrder:NodeKeyStatusIcon.kt$NodeKeyStatusIcon</ID>
|
||||
<ID>ComposableParamOrder:NodeDetailScreen.kt$DeviceActions</ID>
|
||||
<ID>ComposableParamOrder:NodeDetailScreen.kt$EnvironmentMetrics</ID>
|
||||
<ID>ComposableParamOrder:NodeDetailScreen.kt$NodeActionButton</ID>
|
||||
<ID>ComposableParamOrder:NodeDetailScreen.kt$NodeDetailList</ID>
|
||||
<ID>ComposableParamOrder:PaxMetrics.kt$PaxMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout</ID>
|
||||
<ID>ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter</ID>
|
||||
<ID>ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo</ID>
|
||||
<ID>ComposableParamOrder:SettingsItem.kt$SettingsItem</ID>
|
||||
<ID>ComposableParamOrder:SignalInfo.kt$SignalInfo</ID>
|
||||
<ID>ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart</ID>
|
||||
<ID>ComposableParamOrder:TopLevelNavIcon.kt$ConnectionsNavIcon</ID>
|
||||
<ID>ComposableParamOrder:TracerouteButton.kt$TracerouteButton</ID>
|
||||
<ID>ComposableParamOrder:WarningDialog.kt$WarningDialog</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>
|
||||
|
|
@ -57,7 +46,7 @@
|
|||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
||||
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
|
||||
<ID>EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }</ID>
|
||||
<ID>EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }</ID>
|
||||
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
|
||||
<ID>FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
|
||||
<ID>FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
|
||||
|
|
@ -84,8 +73,8 @@
|
|||
<ID>LambdaParameterEventTrailing:Message.kt$onClick</ID>
|
||||
<ID>LambdaParameterEventTrailing:Message.kt$onSendMessage</ID>
|
||||
<ID>LambdaParameterEventTrailing:MessageList.kt$onReply</ID>
|
||||
<ID>LambdaParameterEventTrailing:NodeDetail.kt$onClick</ID>
|
||||
<ID>LambdaParameterEventTrailing:NodeDetail.kt$onSaveNotes</ID>
|
||||
<ID>LambdaParameterEventTrailing:NodeDetailScreen.kt$onClick</ID>
|
||||
<ID>LambdaParameterEventTrailing:NodeDetailScreen.kt$onSaveNotes</ID>
|
||||
<ID>LambdaParameterInRestartableEffect:Channel.kt$onConfirm</ID>
|
||||
<ID>LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged</ID>
|
||||
<ID>LargeClass:MeshService.kt$MeshService : Service</ID>
|
||||
|
|
@ -145,7 +134,7 @@
|
|||
<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.<no name provided>$2500</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth.<no name provided>$2500</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
|
||||
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
|
||||
|
|
@ -158,7 +147,7 @@
|
|||
<ID>MagicNumber:TCPInterface.kt$TCPInterface$500</ID>
|
||||
<ID>MagicNumber:UIState.kt$4</ID>
|
||||
<ID>MatchingDeclarationName:MeshServiceStarter.kt$ServiceStarter : Worker</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 >= 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:BluetoothInterface.kt$/* Info for the esp32 device side code. See that source for the 'gold' standard docs on this interface. MeshBluetoothService UUID 6ba1b218-15a8-461f-9fa8-5dcae273eafd FIXME - notify vs indication for fromradio output. Using notify for now, not sure if that is best FIXME - in the esp32 mesh management code, occasionally mirror the current net db to flash, so that if we reboot we still have a good guess of users who are out there. FIXME - make sure this protocol is guaranteed robust and won't drop packets "According to the BLE specification the notification length can be max ATT_MTU - 3. The 3 bytes subtracted is the 3-byte header(OP-code (operation, 1 byte) and the attribute handle (2 bytes)). In BLE 4.1 the ATT_MTU is 23 bytes (20 bytes for payload), but in BLE 4.2 the ATT_MTU can be negotiated up to 247 bytes." MAXPACKET is 256? look into what the lora lib uses. FIXME Characteristics: UUID properties description 8ba2bcc2-ee02-4a55-a531-c525c5e454d5 read fromradio - contains a newly received packet destined towards the phone (up to MAXPACKET bytes? per packet). After reading the esp32 will put the next packet in this mailbox. If the FIFO is empty it will put an empty packet in this mailbox. f75c76d2-129e-4dad-a1dd-7866124401e7 write toradio - write ToRadio protobufs to this charstic to send them (up to MAXPACKET len) ed9da18c-a800-4f66-a670-aa7547e34453 read|notify|write fromnum - the current packet # in the message waiting inside fromradio, if the phone sees this notify it should read messages until it catches up with this number. The phone can write to this register to go backwards up to FIXME packets, to handle the rare case of a fromradio packet was dropped after the esp32 callback was called, but before it arrives at the phone. If the phone writes to this register the esp32 will discard older packets and put the next packet >= fromnum in fromradio. When the esp32 advances fromnum, it will delay doing the notify by 100ms, in the hopes that the notify will never actally need to be sent if the phone is already pulling from fromradio. Note: that if the phone ever sees this number decrease, it means the esp32 has rebooted. Re: queue management Not all messages are kept in the fromradio queue (filtered based on SubPacket): * only the most recent Position and User messages for a particular node are kept * all Data SubPackets are kept * No WantNodeNum / DenyNodeNum messages are kept A variable keepAllPackets, if set to true will suppress this behavior and instead keep everything for forwarding to the phone (for debugging) */</ID>
|
||||
<ID>ModifierClickableOrder:Channel.kt$clickable(onClick = onClick)</ID>
|
||||
<ID>ModifierMissing:BLEDevices.kt$BLEDevices</ID>
|
||||
<ID>ModifierMissing:Channel.kt$ChannelScreen</ID>
|
||||
|
|
@ -185,8 +174,7 @@
|
|||
<ID>ModifierMissing:MessageActions.kt$ReplyButton</ID>
|
||||
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
|
||||
<ID>ModifierMissing:NetworkDevices.kt$NetworkDevices</ID>
|
||||
<ID>ModifierMissing:NodeScreen.kt$NodeScreen</ID>
|
||||
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>
|
||||
<ID>ModifierMissing:NodeListScreen.kt$NodeListScreen</ID>
|
||||
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsItem</ID>
|
||||
<ID>ModifierMissing:PaxMetrics.kt$PaxMetricsScreen</ID>
|
||||
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen</ID>
|
||||
|
|
@ -199,9 +187,6 @@
|
|||
<ID>ModifierMissing:Reaction.kt$ReactionDialog</ID>
|
||||
<ID>ModifierMissing:SecurityConfigItemList.kt$SecurityConfigScreen</ID>
|
||||
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
|
||||
<ID>ModifierMissing:SettingsItem.kt$SettingsItem</ID>
|
||||
<ID>ModifierMissing:SettingsItem.kt$SettingsItemDetail</ID>
|
||||
<ID>ModifierMissing:SettingsItem.kt$SettingsItemSwitch</ID>
|
||||
<ID>ModifierMissing:SettingsScreen.kt$SettingsScreen</ID>
|
||||
<ID>ModifierMissing:Share.kt$ShareScreen</ID>
|
||||
<ID>ModifierMissing:SignalMetrics.kt$SignalMetricsScreen</ID>
|
||||
|
|
@ -217,11 +202,10 @@
|
|||
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:SignalInfo.kt$modifier = modifier</ID>
|
||||
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)</ID>
|
||||
<ID>ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)</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 < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</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 < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</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: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>
|
||||
|
|
@ -229,9 +213,9 @@
|
|||
<ID>ModifierReused:EnvironmentCharts.kt$MetricPlottingCanvas( modifier = modifier.width(dp), telemetries = telemetries, graphData = graphData, selectedTime = selectedTime, oldest = oldest, timeDiff = timeDiff, rightMin = rightMin, rightMax = rightMax, )</ID>
|
||||
<ID>ModifierReused:EnvironmentCharts.kt$TimeAxisOverlay(modifier = modifier.width(dp), oldest = oldest, newest = newest, selectedTime.lineInterval())</ID>
|
||||
<ID>ModifierReused:PaxMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray })</ID>
|
||||
<ID>ModifierReused:PaxMetrics.kt$Row(modifier = modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f)) { YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(start = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) Box( contentAlignment = Alignment.TopStart, modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(CHART_WEIGHT), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray }) TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval()) Canvas(modifier = Modifier.width(dp).fillMaxHeight()) { val width = size.width val height = size.height fun xForTime(t: Int): Float = if (maxTime == minTime) width / 2 else (t - minTime).toFloat() / (maxTime - minTime) * width fun yForValue(v: Int): Float = height - (v - minValue) / (maxValue - minValue) * height fun drawLine(series: List<Pair<Int, Int>>, color: Color) { for (i in 1 until series.size) { drawLine( color = color, start = Offset(xForTime(series[i - 1].first), yForValue(series[i - 1].second)), end = Offset(xForTime(series[i].first), yForValue(series[i].second)), strokeWidth = 2.dp.toPx(), ) } } drawLine(bleSeries, PaxSeries.BLE.color) drawLine(wifiSeries, PaxSeries.WIFI.color) drawLine(totalSeries, PaxSeries.PAX.color) } } YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(end = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) }</ID>
|
||||
<ID>ModifierReused:PaxMetrics.kt$Row(modifier = modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f)) { YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(start = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) Box( contentAlignment = Alignment.TopStart, modifier = Modifier.horizontalScroll(state = scrollState, reverseScrolling = true).weight(CHART_WEIGHT), ) { HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { Color.LightGray }) TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval()) Canvas(modifier = Modifier.width(dp).fillMaxHeight()) { val width = size.width val height = size.height fun xForTime(t: Int): Float = if (maxTime == minTime) width / 2 else (t - minTime).toFloat() / (maxTime - minTime) * width fun yForValue(v: Int): Float = height - (v - minValue) / (maxValue - minValue) * height fun drawLine(series: List<Pair<Int, Int>>, color: Color) { for (i in 1 until series.size) { drawLine( color = color, start = Offset(xForTime(series[i - 1].first), yForValue(series[i - 1].second)), end = Offset(xForTime(series[i].first), yForValue(series[i].second)), strokeWidth = 2.dp.toPx(), ) } } drawLine(bleSeries, PaxSeries.BLE.color) drawLine(wifiSeries, PaxSeries.WIFI.color) drawLine(totalSeries, PaxSeries.PAX.color) } } YAxisLabels( modifier = Modifier.weight(Y_AXIS_WEIGHT).fillMaxHeight().padding(end = 8.dp), labelColor = MaterialTheme.colorScheme.onSurface, minValue = minValue, maxValue = maxValue, ) }</ID>
|
||||
<ID>ModifierReused:PaxMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = minTime, newest = maxTime, timeFrame.lineInterval())</ID>
|
||||
<ID>ModifierReused:PowerMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width val height = size.height /* Voltage */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveVoltage(selectedChannel, telemetry) - voltageMin) / voltageDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = VOLTAGE_COLOR, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } /* Current */ index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Power.CURRENT.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</ID>
|
||||
<ID>ModifierReused:PowerMetrics.kt$Canvas(modifier = modifier.width(dp)) { val width = size.width val height = size.height /* Voltage */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveVoltage(selectedChannel, telemetry) - voltageMin) / voltageDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = VOLTAGE_COLOR, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } /* Current */ index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = (retrieveCurrent(selectedChannel, telemetry) - Power.CURRENT.min) / currentDiff val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Power.CURRENT.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }</ID>
|
||||
<ID>ModifierReused:PowerMetrics.kt$HorizontalLinesOverlay(modifier.width(dp), lineColors = List(size = 5) { graphColor })</ID>
|
||||
<ID>ModifierReused:PowerMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())</ID>
|
||||
<ID>ModifierReused:PowerMetrics.kt$YAxisLabels( modifier = modifier.weight(weight = Y_AXIS_WEIGHT), Power.CURRENT.color, minValue = Power.CURRENT.min, maxValue = Power.CURRENT.max, )</ID>
|
||||
|
|
@ -247,8 +231,8 @@
|
|||
<ID>MultipleEmitters:CommonCharts.kt$LegendLabel</ID>
|
||||
<ID>MultipleEmitters:DeviceMetrics.kt$DeviceMetricsChart</ID>
|
||||
<ID>MultipleEmitters:EnvironmentCharts.kt$EnvironmentMetricsChart</ID>
|
||||
<ID>MultipleEmitters:NodeDetail.kt$EncryptionErrorContent</ID>
|
||||
<ID>MultipleEmitters:NodeDetail.kt$MetricsSection</ID>
|
||||
<ID>MultipleEmitters:NodeDetailScreen.kt$EncryptionErrorContent</ID>
|
||||
<ID>MultipleEmitters:NodeDetailScreen.kt$MetricsSection</ID>
|
||||
<ID>MultipleEmitters:PaxMetrics.kt$PaxMetricsChart</ID>
|
||||
<ID>MultipleEmitters:PowerMetrics.kt$PowerMetricsChart</ID>
|
||||
<ID>MultipleEmitters:RadioConfig.kt$RadioConfigItemList</ID>
|
||||
|
|
@ -299,28 +283,15 @@
|
|||
<ID>ParameterNaming:MapReportingPreference.kt$onPublishIntervalSecsChanged</ID>
|
||||
<ID>ParameterNaming:MapReportingPreference.kt$onShouldReportLocationChanged</ID>
|
||||
<ID>ParameterNaming:MessageList.kt$onUnreadChanged</ID>
|
||||
<ID>ParameterNaming:NodeDetail.kt$onFirmwareSelected</ID>
|
||||
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
|
||||
<ID>ParameterNaming:NodeDetailScreen.kt$onFirmwareSelected</ID>
|
||||
<ID>ParameterNaming:PositionPrecisionPreference.kt$onValueChanged</ID>
|
||||
<ID>ParameterNaming:UsbDevices.kt$onDeviceSelected</ID>
|
||||
<ID>ParameterNaming:WelcomeScreen.kt$onGetStarted</ID>
|
||||
<ID>PreviewAnnotationNaming:LargeFontPreview.kt$LargeFontPreview$LargeFontPreview</ID>
|
||||
<ID>PreviewPublic:Channel.kt$ModemPresetInfoPreview</ID>
|
||||
<ID>PreviewPublic:ElevationInfo.kt$ElevationInfoPreview</ID>
|
||||
<ID>PreviewPublic:EmptyStateContent.kt$EmptyStateContentPreview</ID>
|
||||
<ID>PreviewPublic:LastHeardInfo.kt$LastHeardInfoPreview</ID>
|
||||
<ID>PreviewPublic:LinkedCoordinates.kt$LinkedCoordinatesPreview</ID>
|
||||
<ID>PreviewPublic:MapReportingPreference.kt$MapReportingPreview</ID>
|
||||
<ID>PreviewPublic:NodeChip.kt$NodeChipPreview</ID>
|
||||
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
|
||||
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>
|
||||
<ID>PreviewPublic:NodeStatusIcons.kt$StatusIconsPreview</ID>
|
||||
<ID>PreviewPublic:Reaction.kt$ReactionItemPreview</ID>
|
||||
<ID>PreviewPublic:Reaction.kt$ReactionRowPreview</ID>
|
||||
<ID>PreviewPublic:SatelliteCountInfo.kt$SatelliteCountInfoPreview</ID>
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoPreview</ID>
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSelfPreview</ID>
|
||||
<ID>PreviewPublic:SignalInfo.kt$SignalInfoSimplePreview</ID>
|
||||
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
|
||||
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>SwallowedException:BluetoothInterface.kt$BluetoothInterface$ex: CancellationException</ID>
|
||||
|
|
@ -339,7 +310,7 @@
|
|||
<ID>TooGenericExceptionCaught:LocationRepository.kt$LocationRepository$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MQTTRepository.kt$MQTTRepository$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$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>
|
||||
|
|
@ -347,29 +318,28 @@
|
|||
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService$throw Exception("Can't set user without a NodeInfo")</ID>
|
||||
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService.<no name provided>$throw Exception("Port numbers must be non-zero!")</ID>
|
||||
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService.<no name provided>$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:BluetoothInterface.kt$BluetoothInterface : IRadioInterface</ID>
|
||||
<ID>TooManyFunctions:MeshService.kt$MeshService : Service</ID>
|
||||
<ID>TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub</ID>
|
||||
<ID>TooManyFunctions:MeshService.kt$MeshService$<no name provided> : Stub</ID>
|
||||
<ID>TooManyFunctions:MessageViewModel.kt$MessageViewModel : ViewModel</ID>
|
||||
<ID>TooManyFunctions:NodeDetail.kt$com.geeksville.mesh.ui.node.NodeDetail.kt</ID>
|
||||
<ID>TooManyFunctions:NodesViewModel.kt$NodesViewModel : ViewModel</ID>
|
||||
<ID>TooManyFunctions:NodeDetailScreen.kt$com.geeksville.mesh.ui.node.NodeDetailScreen.kt</ID>
|
||||
<ID>TooManyFunctions:RadioConfigViewModel.kt$RadioConfigViewModel : ViewModel</ID>
|
||||
<ID>TooManyFunctions:RadioInterfaceService.kt$RadioInterfaceService</ID>
|
||||
<ID>TooManyFunctions:SafeBluetooth.kt$SafeBluetooth : Closeable</ID>
|
||||
<ID>TooManyFunctions:UIState.kt$UIViewModel : ViewModel</ID>
|
||||
<ID>TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"</ID>
|
||||
<ID>UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit</ID>
|
||||
<ID>UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit</ID>
|
||||
<ID>UnusedParameter:ChannelSettingsItemList.kt$title: String</ID>
|
||||
<ID>UnusedParameter:DropDownPreference.kt$modifier: Modifier = Modifier</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule</ID>
|
||||
<ID>ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)</ID>
|
||||
<ID>ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)</ID>
|
||||
<ID>ViewModelInjection:DebugSearch.kt$viewModel</ID>
|
||||
<ID>Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
|
||||
<ID>Wrapping:Message.kt${ event -> when (event) { is MessageScreenEvent.SendMessage -> { viewModel.sendMessage(event.text, contactKey, event.replyingToPacketId) if (event.replyingToPacketId != null) replyingToPacketId = null messageInputState.clearText() } is MessageScreenEvent.SendReaction -> viewModel.sendReaction(event.emoji, event.messageId, contactKey) is MessageScreenEvent.DeleteMessages -> { viewModel.deleteMessages(event.ids) selectedMessageIds.value = emptySet() showDeleteDialog = false } is MessageScreenEvent.ClearUnreadCount -> viewModel.clearUnreadCount(contactKey, event.lastReadMessageId) is MessageScreenEvent.NodeDetails -> navigateToNodeDetails(event.node.num) is MessageScreenEvent.SetTitle -> viewModel.setTitle(event.title) is MessageScreenEvent.NavigateToMessages -> navigateToMessages(event.contactKey) is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum) MessageScreenEvent.NavigateBack -> onNavigateBack() is MessageScreenEvent.CopyToClipboard -> { clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text)) selectedMessageIds.value = emptySet() } } }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ import androidx.compose.ui.test.assertIsDisplayed
|
|||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.message.components.MessageItem
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageItemTest {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ import com.geeksville.mesh.ui.map.components.WaypointMarkers
|
|||
import com.geeksville.mesh.ui.metrics.HEADING_DEG
|
||||
import com.geeksville.mesh.ui.metrics.formatPositionTime
|
||||
import com.geeksville.mesh.ui.node.DEG_D
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.waypoint
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
|
|
@ -112,6 +111,7 @@ import org.meshtastic.core.model.util.mpsToKmph
|
|||
import org.meshtastic.core.model.util.mpsToMph
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.LayerType
|
||||
import org.meshtastic.feature.map.LocationPermissionsHandler
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.map.NodeClusterItem
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
|
||||
@Composable
|
||||
fun ClusterItemsListDialog(
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.geeksville.mesh.ui.map.NodeClusterItem
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.google.maps.android.clustering.Cluster
|
||||
import com.google.maps.android.clustering.view.DefaultClusterRenderer
|
||||
import com.google.maps.android.compose.Circle
|
||||
import com.google.maps.android.compose.MapsComposeExperimentalApi
|
||||
import com.google.maps.android.compose.clustering.Clustering
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
|
||||
@OptIn(MapsComposeExperimentalApi::class)
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ import com.geeksville.mesh.ui.metrics.PowerMetricsScreen
|
|||
import com.geeksville.mesh.ui.metrics.SignalMetricsScreen
|
||||
import com.geeksville.mesh.ui.metrics.TracerouteLogScreen
|
||||
import com.geeksville.mesh.ui.node.NodeDetailScreen
|
||||
import com.geeksville.mesh.ui.node.NodeListScreen
|
||||
import com.geeksville.mesh.ui.node.NodeMapScreen
|
||||
import com.geeksville.mesh.ui.node.NodeScreen
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
|
|
@ -64,7 +64,7 @@ fun NavGraphBuilder.nodesGraph(navController: NavHostController, uiViewModel: UI
|
|||
composable<NodesRoutes.Nodes>(
|
||||
deepLinks = listOf(navDeepLink<NodesRoutes.Nodes>(basePath = "$DEEP_LINK_BASE_URI/nodes")),
|
||||
) {
|
||||
NodeScreen(navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) })
|
||||
NodeListScreen(navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) })
|
||||
}
|
||||
nodeDetailGraph(navController, uiViewModel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
|
|||
|
|
@ -1,134 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
import org.meshtastic.core.ui.component.Snr
|
||||
import org.meshtastic.core.ui.component.determineSignalQuality
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
const val MAX_VALID_SNR = 100F
|
||||
const val MAX_VALID_RSSI = 0
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SignalInfo(modifier: Modifier = Modifier, node: Node, isThisNode: Boolean) {
|
||||
val text =
|
||||
if (isThisNode) {
|
||||
stringResource(R.string.channel_air_util)
|
||||
.format(node.deviceMetrics.channelUtilization, node.deviceMetrics.airUtilTx)
|
||||
} else {
|
||||
buildList {
|
||||
val hopsString =
|
||||
"%s: %s"
|
||||
.format(
|
||||
stringResource(R.string.hops_away),
|
||||
if (node.hopsAway == -1) {
|
||||
"?"
|
||||
} else {
|
||||
node.hopsAway.toString()
|
||||
},
|
||||
)
|
||||
if (node.channel > 0) {
|
||||
add("ch:${node.channel}")
|
||||
}
|
||||
if (node.hopsAway != 0) add(hopsString)
|
||||
}
|
||||
.joinToString(" ")
|
||||
}
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (text.isNotEmpty()) {
|
||||
Text(text = text, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
/* We only know the Signal Quality from direct nodes aka 0 hop. */
|
||||
if (node.hopsAway <= 0) {
|
||||
if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) {
|
||||
val quality = determineSignalQuality(node.snr, node.rssi)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Snr(node.snr)
|
||||
Rssi(node.rssi)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = quality.imageVector,
|
||||
contentDescription = stringResource(R.string.signal_quality),
|
||||
tint = quality.color.invoke(),
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.signal)} ${stringResource(quality.nameRes)}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun SignalInfoSimplePreview() {
|
||||
AppTheme {
|
||||
SignalInfo(
|
||||
node = Node(num = 1, lastHeard = 0, channel = 0, snr = 12.5F, rssi = -42, hopsAway = 0),
|
||||
isThisNode = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun SignalInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
|
||||
AppTheme { SignalInfo(node = node, isThisNode = false) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewLightDark
|
||||
fun SignalInfoSelfPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
|
||||
AppTheme { SignalInfo(node = node, isThisNode = true) }
|
||||
}
|
||||
|
|
@ -1,23 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.common.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Preview(name = "Large Font", fontScale = 2f)
|
||||
annotation class LargeFontPreview
|
||||
|
|
@ -1,158 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.common.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.deviceMetrics
|
||||
import com.geeksville.mesh.environmentMetrics
|
||||
import com.geeksville.mesh.paxcount
|
||||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.user
|
||||
import com.google.protobuf.ByteString
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
|
||||
import kotlin.random.Random
|
||||
|
||||
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
||||
val mickeyMouse =
|
||||
Node(
|
||||
num = 1955,
|
||||
user =
|
||||
user {
|
||||
id = "mickeyMouseId"
|
||||
longName = "Mickey Mouse"
|
||||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
role = ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
},
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338125110
|
||||
longitudeI = -1179189760
|
||||
altitude = 138
|
||||
satsInView = 4
|
||||
},
|
||||
lastHeard = currentTime(),
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 0,
|
||||
)
|
||||
|
||||
val minnieMouse =
|
||||
mickeyMouse.copy(
|
||||
num = Random.nextInt(),
|
||||
user =
|
||||
user {
|
||||
longName = "Minnie Mouse"
|
||||
shortName = "MiMo"
|
||||
id = "minnieMouseId"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
},
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
position = position {},
|
||||
hopsAway = 1,
|
||||
)
|
||||
|
||||
private val donaldDuck =
|
||||
Node(
|
||||
num = Random.nextInt(),
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338052347
|
||||
longitudeI = -1179208460
|
||||
altitude = 121
|
||||
satsInView = 66
|
||||
},
|
||||
lastHeard = currentTime() - 300,
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
user =
|
||||
user {
|
||||
id = "donaldDuckId"
|
||||
longName = "Donald Duck, the Grand Duck of the Ducks"
|
||||
shortName = "DoDu"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
|
||||
},
|
||||
environmentMetrics =
|
||||
environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
},
|
||||
paxcounter =
|
||||
paxcount {
|
||||
wifi = 30
|
||||
ble = 39
|
||||
uptime = 420
|
||||
},
|
||||
isFavorite = true,
|
||||
hopsAway = 2,
|
||||
)
|
||||
|
||||
private val unknown =
|
||||
donaldDuck.copy(
|
||||
user =
|
||||
user {
|
||||
id = "myId"
|
||||
longName = "Meshtastic myId"
|
||||
shortName = "myId"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
environmentMetrics = environmentMetrics {},
|
||||
paxcounter = paxcount {},
|
||||
)
|
||||
|
||||
private val almostNothing = Node(num = Random.nextInt())
|
||||
|
||||
override val values: Sequence<Node>
|
||||
get() =
|
||||
sequenceOf(
|
||||
mickeyMouse, // "this" node
|
||||
unknown,
|
||||
almostNothing,
|
||||
minnieMouse,
|
||||
donaldDuck,
|
||||
)
|
||||
}
|
||||
|
|
@ -70,7 +70,6 @@ import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
|
|||
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
|
||||
import com.geeksville.mesh.ui.connections.components.NetworkDevices
|
||||
import com.geeksville.mesh.ui.connections.components.UsbDevices
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -79,6 +78,7 @@ import org.meshtastic.core.navigation.Route
|
|||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItem
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
|
||||
fun String?.isIPAddress(): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
import org.meshtastic.core.ui.component.MaterialBluetoothSignalInfo
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ui.common.components.SecurityIcon
|
||||
import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -107,6 +106,7 @@ import org.meshtastic.core.model.DataPacket
|
|||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.component.NodeKeyStatusIcon
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
private const val MESSAGE_CHARACTER_LIMIT_BYTES = 200
|
||||
|
|
|
|||
|
|
@ -49,16 +49,16 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.database.entity.Reaction
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MDText
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
import org.meshtastic.core.ui.component.Snr
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.MessageItemColors
|
||||
|
||||
|
|
|
|||
|
|
@ -134,13 +134,6 @@ import com.geeksville.mesh.MeshProtos
|
|||
import com.geeksville.mesh.model.MetricsState
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.node.components.TracerouteButton
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.geeksville.mesh.util.thenIf
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
|
|
@ -163,12 +156,20 @@ import org.meshtastic.core.navigation.Route
|
|||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItem
|
||||
import org.meshtastic.core.ui.component.SettingsItemDetail
|
||||
import org.meshtastic.core.ui.component.SettingsItemSwitch
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.feature.node.component.NodeActionDialogs
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
import org.meshtastic.feature.node.component.TracerouteButton
|
||||
import org.meshtastic.feature.node.detail.NodeDetailViewModel
|
||||
import timber.log.Timber
|
||||
|
||||
private data class VectorMetricInfo(
|
||||
|
|
@ -1,131 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NodeDetailViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
|
||||
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()
|
||||
|
||||
fun handleNodeMenuAction(action: NodeMenuAction) {
|
||||
when (action) {
|
||||
is NodeMenuAction.Remove -> removeNode(action.node.num)
|
||||
is NodeMenuAction.Ignore -> ignoreNode(action.node)
|
||||
is NodeMenuAction.Favorite -> favoriteNode(action.node)
|
||||
is NodeMenuAction.RequestUserInfo -> requestUserInfo(action.node.num)
|
||||
is NodeMenuAction.RequestPosition -> requestPosition(action.node.num)
|
||||
is NodeMenuAction.TraceRoute -> {
|
||||
requestTraceroute(action.node.num)
|
||||
_lastTraceRouteTime.value = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun setNodeNotes(nodeNum: Int, notes: String) = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
nodeRepository.setNodeNotes(nodeNum, notes)
|
||||
} catch (ex: java.io.IOException) {
|
||||
Timber.e("Set node notes IO error: ${ex.message}")
|
||||
} catch (ex: java.sql.SQLException) {
|
||||
Timber.e("Set node notes SQL error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.i("Removing node '$nodeNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@launch
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
nodeRepository.deleteNode(nodeNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Remove node error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ignoreNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Ignore node error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun favoriteNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Favorite node error")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestUserInfo(destNum: Int) {
|
||||
Timber.i("Requesting UserInfo for '$destNum'")
|
||||
try {
|
||||
serviceRepository.meshService?.requestUserInfo(destNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request NodeInfo error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPosition(destNum: Int, position: Position = Position(0.0, 0.0, 0)) {
|
||||
Timber.i("Requesting position for '$destNum'")
|
||||
try {
|
||||
serviceRepository.meshService?.requestPosition(destNum, position)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request position error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestTraceroute(destNum: Int) {
|
||||
Timber.i("Requesting traceroute for '$destNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return
|
||||
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Request traceroute error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,9 +59,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
|
||||
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
|
||||
import com.geeksville.mesh.ui.node.components.NodeItem
|
||||
import com.geeksville.mesh.ui.sharing.AddContactFAB
|
||||
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
|
@ -70,24 +67,28 @@ import org.meshtastic.core.service.ConnectionState
|
|||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.feature.node.component.NodeActionDialogs
|
||||
import org.meshtastic.feature.node.component.NodeFilterTextField
|
||||
import org.meshtastic.feature.node.component.NodeItem
|
||||
import org.meshtastic.feature.node.list.NodeListViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeDetails: (Int) -> Unit) {
|
||||
val state by nodesViewModel.nodesUiState.collectAsStateWithLifecycle()
|
||||
fun NodeListScreen(viewModel: NodeListViewModel = hiltViewModel(), navigateToNodeDetails: (Int) -> Unit) {
|
||||
val state by viewModel.nodesUiState.collectAsStateWithLifecycle()
|
||||
|
||||
val nodes by nodesViewModel.nodeList.collectAsStateWithLifecycle()
|
||||
val ourNode by nodesViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val onlineNodeCount by nodesViewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by nodesViewModel.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val unfilteredNodes by nodesViewModel.unfilteredNodeList.collectAsStateWithLifecycle()
|
||||
val nodes by viewModel.nodeList.collectAsStateWithLifecycle()
|
||||
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val unfilteredNodes by viewModel.unfilteredNodeList.collectAsStateWithLifecycle()
|
||||
val ignoredNodeCount = unfilteredNodes.count { it.isIgnored }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val currentTimeMillis = rememberTimeTickWithLifecycle()
|
||||
val connectionState by nodesViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
|
||||
val isScrollInProgress by remember {
|
||||
derivedStateOf { listState.isScrollInProgress && (listState.canScrollForward || listState.canScrollBackward) }
|
||||
|
|
@ -109,7 +110,7 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
|
|||
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
|
||||
val shareCapable = firmwareVersion.supportsQrCodeSharing()
|
||||
val scannedContact: AdminProtos.SharedContact? by
|
||||
nodesViewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
AddContactFAB(
|
||||
unfilteredNodes = unfilteredNodes,
|
||||
scannedContact = scannedContact,
|
||||
|
|
@ -118,8 +119,8 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
|
|||
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onSharedContactImport = { contact -> nodesViewModel.addSharedContact(contact) },
|
||||
onSharedContactRequested = { contact -> nodesViewModel.setSharedContactRequested(contact) },
|
||||
onSharedContactImport = { contact -> viewModel.addSharedContact(contact) },
|
||||
onSharedContactRequested = { contact -> viewModel.setSharedContactRequested(contact) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
|
|
@ -135,17 +136,17 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
|
|||
.background(MaterialTheme.colorScheme.surfaceDim)
|
||||
.padding(8.dp),
|
||||
filterText = state.filter.filterText,
|
||||
onTextChange = nodesViewModel::setNodeFilterText,
|
||||
onTextChange = viewModel::setNodeFilterText,
|
||||
currentSortOption = state.sort,
|
||||
onSortSelect = nodesViewModel::setSortOption,
|
||||
onSortSelect = viewModel::setSortOption,
|
||||
includeUnknown = state.filter.includeUnknown,
|
||||
onToggleIncludeUnknown = nodesViewModel::toggleIncludeUnknown,
|
||||
onToggleIncludeUnknown = viewModel::toggleIncludeUnknown,
|
||||
onlyOnline = state.filter.onlyOnline,
|
||||
onToggleOnlyOnline = nodesViewModel::toggleOnlyOnline,
|
||||
onToggleOnlyOnline = viewModel::toggleOnlyOnline,
|
||||
onlyDirect = state.filter.onlyDirect,
|
||||
onToggleOnlyDirect = nodesViewModel::toggleOnlyDirect,
|
||||
onToggleOnlyDirect = viewModel::toggleOnlyDirect,
|
||||
showIgnored = state.filter.showIgnored,
|
||||
onToggleShowIgnored = nodesViewModel::toggleShowIgnored,
|
||||
onToggleShowIgnored = viewModel::toggleShowIgnored,
|
||||
ignoredNodeCount = ignoredNodeCount,
|
||||
)
|
||||
}
|
||||
|
|
@ -165,9 +166,9 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
|
|||
displayIgnoreDialog = false
|
||||
displayRemoveDialog = false
|
||||
},
|
||||
onConfirmFavorite = nodesViewModel::favoriteNode,
|
||||
onConfirmIgnore = nodesViewModel::ignoreNode,
|
||||
onConfirmRemove = { nodesViewModel.removeNode(it.num) },
|
||||
onConfirmFavorite = viewModel::favoriteNode,
|
||||
onConfirmIgnore = viewModel::ignoreNode,
|
||||
onConfirmRemove = { viewModel.removeNode(it.num) },
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
|
@ -1,212 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.AdminProtos
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NodesViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
) : ViewModel() {
|
||||
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
val onlineNodeCount =
|
||||
nodeRepository.onlineNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
val totalNodeCount =
|
||||
nodeRepository.totalNodeCount.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
||||
val sharedContactRequested = _sharedContactRequested.asStateFlow()
|
||||
|
||||
private val nodeSortOption =
|
||||
uiPreferencesDataSource.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
||||
|
||||
private val nodeFilterText = MutableStateFlow("")
|
||||
private val includeUnknown = uiPreferencesDataSource.includeUnknown
|
||||
private val onlyOnline = uiPreferencesDataSource.onlyOnline
|
||||
private val onlyDirect = uiPreferencesDataSource.onlyDirect
|
||||
private val showIgnored = uiPreferencesDataSource.showIgnored
|
||||
|
||||
private val nodeFilter: Flow<NodeFilterState> =
|
||||
combine(nodeFilterText, includeUnknown, onlyOnline, onlyDirect, showIgnored) {
|
||||
filterText,
|
||||
includeUnknown,
|
||||
onlyOnline,
|
||||
onlyDirect,
|
||||
showIgnored,
|
||||
->
|
||||
NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored)
|
||||
}
|
||||
|
||||
val nodesUiState: StateFlow<NodesUiState> =
|
||||
combine(nodeSortOption, nodeFilter, radioConfigRepository.deviceProfileFlow) { sort, nodeFilter, profile ->
|
||||
NodesUiState(
|
||||
sort = sort,
|
||||
filter = nodeFilter,
|
||||
distanceUnits = profile.config.display.units.number,
|
||||
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = NodesUiState(),
|
||||
)
|
||||
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
combine(nodeFilter, nodeSortOption, ::Pair)
|
||||
.flatMapLatest { (filter, sort) ->
|
||||
nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list -> list.filter { it.isIgnored == filter.showIgnored } }
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
nodeRepository
|
||||
.getNodes()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
fun setNodeFilterText(text: String) {
|
||||
nodeFilterText.value = text
|
||||
}
|
||||
|
||||
fun toggleIncludeUnknown() {
|
||||
uiPreferencesDataSource.setIncludeUnknown(!includeUnknown.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyOnline() {
|
||||
uiPreferencesDataSource.setOnlyOnline(!onlyOnline.value)
|
||||
}
|
||||
|
||||
fun toggleOnlyDirect() {
|
||||
uiPreferencesDataSource.setOnlyDirect(!onlyDirect.value)
|
||||
}
|
||||
|
||||
fun toggleShowIgnored() {
|
||||
uiPreferencesDataSource.setShowIgnored(!showIgnored.value)
|
||||
}
|
||||
|
||||
fun setSortOption(sort: NodeSortOption) {
|
||||
uiPreferencesDataSource.setNodeSort(sort.ordinal)
|
||||
}
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) }
|
||||
|
||||
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
|
||||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Favorite node error")
|
||||
}
|
||||
}
|
||||
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e(ex, "Ignore node error")
|
||||
}
|
||||
}
|
||||
|
||||
fun removeNode(nodeNum: Int) = viewModelScope.launch(Dispatchers.IO) {
|
||||
Timber.i("Removing node '$nodeNum'")
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@launch
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
nodeRepository.deleteNode(nodeNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Timber.e("Remove node error: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NodesUiState(
|
||||
val sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
val filter: NodeFilterState = NodeFilterState(),
|
||||
val distanceUnits: Int = 0,
|
||||
val tempInFahrenheit: Boolean = false,
|
||||
)
|
||||
|
||||
data class NodeFilterState(
|
||||
val filterText: String = "",
|
||||
val includeUnknown: Boolean = false,
|
||||
val onlyOnline: Boolean = false,
|
||||
val onlyDirect: Boolean = false,
|
||||
val showIgnored: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,43 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.SocialDistance
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun DistanceInfo(distance: String, modifier: Modifier = Modifier) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = Icons.Rounded.SocialDistance,
|
||||
contentDescription = stringResource(R.string.distance),
|
||||
text = distance,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DistanceInfoPreview() {
|
||||
AppTheme { DistanceInfo(distance = "423 mi.") }
|
||||
}
|
||||
|
|
@ -1,51 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
||||
@Composable
|
||||
fun ElevationInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
altitude: Int,
|
||||
system: DisplayUnits,
|
||||
suffix: String = stringResource(R.string.elevation_suffix),
|
||||
) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = MeshtasticIcons.Elevation,
|
||||
contentDescription = stringResource(R.string.altitude),
|
||||
text = altitude.metersIn(system).toString(system) + " " + suffix,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ElevationInfoPreview() {
|
||||
MaterialTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
|
||||
}
|
||||
|
|
@ -1,69 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
||||
private const val SIZE_ICON = 20
|
||||
|
||||
@Composable
|
||||
fun IconInfo(
|
||||
icon: ImageVector,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
text: String? = null,
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(SIZE_ICON.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
text?.let {
|
||||
Text(text = it, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun IconInfoPreview() {
|
||||
MaterialTheme {
|
||||
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", content = { Text(text = "100") })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import com.geeksville.mesh.R
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun LastHeardInfo(modifier: Modifier = Modifier, lastHeard: Int, currentTimeMillis: Long) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
|
||||
contentDescription = stringResource(org.meshtastic.core.strings.R.string.node_sort_last_heard),
|
||||
text = formatAgo(lastHeard, currentTimeMillis),
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun LastHeardInfoPreview() {
|
||||
AppTheme {
|
||||
LastHeardInfo(
|
||||
lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600,
|
||||
currentTimeMillis = System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.HyperlinkBlue
|
||||
import timber.log.Timber
|
||||
import java.net.URLEncoder
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude: Double, nodeName: String) {
|
||||
val context = LocalContext.current
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val style =
|
||||
SpanStyle(
|
||||
color = HyperlinkBlue,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
|
||||
val annotatedString = rememberAnnotatedString(latitude, longitude, nodeName, style)
|
||||
|
||||
Text(
|
||||
modifier =
|
||||
modifier.combinedClickable(
|
||||
onClick = { handleClick(context, annotatedString) },
|
||||
onLongClick = {
|
||||
coroutineScope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", annotatedString)))
|
||||
Timber.d("Copied to clipboard")
|
||||
}
|
||||
},
|
||||
),
|
||||
text = annotatedString,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberAnnotatedString(latitude: Double, longitude: Double, nodeName: String, style: SpanStyle) =
|
||||
buildAnnotatedString {
|
||||
pushStringAnnotation(
|
||||
tag = "gps",
|
||||
annotation =
|
||||
"geo:0,0?q=$latitude,$longitude&z=17&label=${
|
||||
URLEncoder.encode(nodeName, "utf-8")
|
||||
}",
|
||||
)
|
||||
withStyle(style = style) {
|
||||
val gpsString = GPSFormat.toDec(latitude, longitude)
|
||||
append(gpsString)
|
||||
}
|
||||
pop()
|
||||
}
|
||||
|
||||
private fun handleClick(context: Context, annotatedString: AnnotatedString) {
|
||||
annotatedString.getStringAnnotations(tag = "gps", start = 0, end = annotatedString.length).firstOrNull()?.let {
|
||||
val uri = it.item.toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
|
||||
|
||||
try {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Timber.d("Failed to open geo intent: $ex")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun LinkedCoordinatesPreview() {
|
||||
AppTheme { LinkedCoordinates(latitude = 37.7749, longitude = -122.4194, nodeName = "Test Node Name") }
|
||||
}
|
||||
|
|
@ -1,94 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
||||
@Composable
|
||||
fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit)? = null) {
|
||||
val (textColor, nodeColor) = node.colors
|
||||
val colors = CardDefaults.cardColors(containerColor = Color(nodeColor), contentColor = Color(textColor))
|
||||
|
||||
val content: @Composable () -> Unit = {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.width(IntrinsicSize.Min)
|
||||
.defaultMinSize(minWidth = 72.dp, minHeight = 32.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = node.user.shortName.ifEmpty { "???" },
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { node.isIgnored },
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick == null) {
|
||||
Card(modifier = modifier, shape = MaterialTheme.shapes.small, colors = colors) { content() }
|
||||
} else {
|
||||
Card(modifier = modifier, shape = MaterialTheme.shapes.small, colors = colors, onClick = { onClick(node) }) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Preview
|
||||
@Composable
|
||||
fun NodeChipPreview() {
|
||||
val user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build()
|
||||
val node =
|
||||
Node(
|
||||
num = 13444,
|
||||
user = user,
|
||||
isIgnored = false,
|
||||
paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
|
||||
environmentMetrics =
|
||||
TelemetryProtos.EnvironmentMetrics.newBuilder().setTemperature(25f).setRelativeHumidity(60f).build(),
|
||||
)
|
||||
NodeChip(node = node)
|
||||
}
|
||||
|
|
@ -1,313 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.common.preview.LargeFontPreview
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun NodeFilterTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
filterText: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
currentSortOption: NodeSortOption,
|
||||
onSortSelect: (NodeSortOption) -> Unit,
|
||||
includeUnknown: Boolean,
|
||||
onToggleIncludeUnknown: () -> Unit,
|
||||
onlyOnline: Boolean,
|
||||
onToggleOnlyOnline: () -> Unit,
|
||||
onlyDirect: Boolean,
|
||||
onToggleOnlyDirect: () -> Unit,
|
||||
showIgnored: Boolean,
|
||||
onToggleShowIgnored: () -> Unit,
|
||||
ignoredNodeCount: Int,
|
||||
) {
|
||||
Column(modifier = modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
Row {
|
||||
NodeFilterTextField(filterText = filterText, onTextChange = onTextChange, modifier = Modifier.weight(1f))
|
||||
|
||||
NodeSortButton(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
currentSortOption = currentSortOption,
|
||||
onSortSelect = onSortSelect,
|
||||
toggles =
|
||||
NodeFilterToggles(
|
||||
includeUnknown = includeUnknown,
|
||||
onToggleIncludeUnknown = onToggleIncludeUnknown,
|
||||
onlyOnline = onlyOnline,
|
||||
onToggleOnlyOnline = onToggleOnlyOnline,
|
||||
onlyDirect = onlyDirect,
|
||||
onToggleOnlyDirect = onToggleOnlyDirect,
|
||||
showIgnored = showIgnored,
|
||||
onToggleShowIgnored = onToggleShowIgnored,
|
||||
ignoredNodeCount = ignoredNodeCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (showIgnored) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceDim)
|
||||
.clickable { onToggleShowIgnored() }
|
||||
.padding(vertical = 16.dp, horizontal = 24.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.node_filter_ignored),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier.defaultMinSize(minHeight = 48.dp).onFocusEvent { isFocused = it.isFocused },
|
||||
value = filterText,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.node_filter_placeholder),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.35F),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(id = R.string.node_filter_placeholder))
|
||||
},
|
||||
onValueChange = onTextChange,
|
||||
trailingIcon = {
|
||||
if (filterText.isNotEmpty() || isFocused) {
|
||||
Icon(
|
||||
Icons.Default.Clear,
|
||||
contentDescription = stringResource(id = R.string.desc_node_filter_clear),
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
onTextChange("")
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onBackground),
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NodeSortButton(
|
||||
currentSortOption: NodeSortOption,
|
||||
onSortSelect: (NodeSortOption) -> Unit,
|
||||
toggles: NodeFilterToggles,
|
||||
modifier: Modifier = Modifier,
|
||||
) = Box(modifier) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(onClick = { expanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = stringResource(R.string.node_sort_button),
|
||||
modifier = Modifier.heightIn(max = 48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 1f)),
|
||||
) {
|
||||
DropdownMenuTitle(text = stringResource(R.string.node_sort_title))
|
||||
|
||||
NodeSortOption.entries.forEach { sort ->
|
||||
DropdownMenuRadio(
|
||||
text = stringResource(id = sort.stringRes),
|
||||
selected = sort == currentSortOption,
|
||||
onClick = { onSortSelect(sort) },
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(MenuDefaults.DropdownMenuItemContentPadding))
|
||||
|
||||
DropdownMenuTitle(text = stringResource(R.string.node_filter_title))
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(R.string.node_filter_include_unknown),
|
||||
checked = toggles.includeUnknown,
|
||||
onClick = toggles.onToggleIncludeUnknown,
|
||||
)
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(R.string.node_filter_only_online),
|
||||
checked = toggles.onlyOnline,
|
||||
onClick = toggles.onToggleOnlyOnline,
|
||||
)
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(R.string.node_filter_only_direct),
|
||||
checked = toggles.onlyDirect,
|
||||
onClick = toggles.onToggleOnlyDirect,
|
||||
)
|
||||
|
||||
DropdownMenuCheck(
|
||||
text = stringResource(R.string.node_filter_show_ignored),
|
||||
checked = toggles.showIgnored,
|
||||
onClick = toggles.onToggleShowIgnored,
|
||||
trailing =
|
||||
if (toggles.ignoredNodeCount > 0) {
|
||||
{
|
||||
Text(
|
||||
text = " (${toggles.ignoredNodeCount})",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropdownMenuTitle(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier =
|
||||
Modifier.height(48.dp)
|
||||
.padding(MenuDefaults.DropdownMenuItemContentPadding)
|
||||
.wrapContentHeight(align = Alignment.CenterVertically),
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropdownMenuRadio(text: String, selected: Boolean, onClick: () -> Unit) {
|
||||
DropdownMenuItem(
|
||||
onClick = onClick,
|
||||
leadingIcon = { RadioButton(selected = selected, onClick = null) },
|
||||
text = { Text(text = text) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropdownMenuCheck(
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
trailing: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = onClick,
|
||||
leadingIcon = { Checkbox(checked = checked, onCheckedChange = null) },
|
||||
trailingIcon = trailing,
|
||||
text = { Text(text = text) },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@LargeFontPreview
|
||||
@Composable
|
||||
private fun NodeFilterTextFieldPreview() {
|
||||
AppTheme {
|
||||
NodeFilterTextField(
|
||||
filterText = "Filter text",
|
||||
onTextChange = {},
|
||||
currentSortOption = NodeSortOption.LAST_HEARD,
|
||||
onSortSelect = {},
|
||||
includeUnknown = false,
|
||||
onToggleIncludeUnknown = {},
|
||||
onlyOnline = false,
|
||||
onToggleOnlyOnline = {},
|
||||
onlyDirect = false,
|
||||
onToggleOnlyDirect = {},
|
||||
showIgnored = false,
|
||||
onToggleShowIgnored = {},
|
||||
ignoredNodeCount = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class NodeFilterToggles(
|
||||
val includeUnknown: Boolean,
|
||||
val onToggleIncludeUnknown: () -> Unit,
|
||||
val onlyOnline: Boolean,
|
||||
val onToggleOnlyOnline: () -> Unit,
|
||||
val onlyDirect: Boolean,
|
||||
val onToggleOnlyDirect: () -> Unit,
|
||||
val showIgnored: Boolean,
|
||||
val onToggleShowIgnored: () -> Unit,
|
||||
val ignoredNodeCount: Int,
|
||||
)
|
||||
|
|
@ -1,240 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.ui.common.components.SignalInfo
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun NodeItem(
|
||||
thisNode: Node?,
|
||||
thatNode: Node,
|
||||
distanceUnits: Int,
|
||||
tempInFahrenheit: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
currentTimeMillis: Long,
|
||||
isConnected: Boolean = false,
|
||||
) {
|
||||
val isFavorite = remember(thatNode) { thatNode.isFavorite }
|
||||
val isIgnored = thatNode.isIgnored
|
||||
val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) }
|
||||
val isThisNode = remember(thatNode) { thisNode?.num == thatNode.num }
|
||||
val system = remember(distanceUnits) { DisplayConfig.DisplayUnits.forNumber(distanceUnits) }
|
||||
val distance =
|
||||
remember(thisNode, thatNode) { thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) }
|
||||
|
||||
var contentColor = MaterialTheme.colorScheme.onSurface
|
||||
val cardColors =
|
||||
if (isThisNode) {
|
||||
thisNode?.colors?.second
|
||||
} else {
|
||||
thatNode.colors.second
|
||||
}
|
||||
?.let {
|
||||
val containerColor = Color(it).copy(alpha = 0.2f)
|
||||
contentColor = contentColorFor(containerColor)
|
||||
CardDefaults.cardColors().copy(containerColor = containerColor, contentColor = contentColor)
|
||||
} ?: (CardDefaults.cardColors())
|
||||
|
||||
val style =
|
||||
if (thatNode.isUnknownUser) {
|
||||
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
|
||||
} else {
|
||||
LocalTextStyle.current
|
||||
}
|
||||
|
||||
val unmessageable =
|
||||
remember(thatNode) {
|
||||
when {
|
||||
thatNode.user.hasIsUnmessagable() -> thatNode.user.isUnmessagable
|
||||
else -> thatNode.user.role.isUnmessageableRole()
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
NodeChip(node = thatNode)
|
||||
|
||||
NodeKeyStatusIcon(
|
||||
hasPKC = thatNode.hasPKC,
|
||||
mismatchKey = thatNode.mismatchKey,
|
||||
publicKey = thatNode.user.publicKey,
|
||||
modifier = Modifier.size(32.dp),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = longName,
|
||||
style =
|
||||
MaterialTheme.typography.titleMediumEmphasized.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
|
||||
softWrap = true,
|
||||
)
|
||||
LastHeardInfo(lastHeard = thatNode.lastHeard, currentTimeMillis = currentTimeMillis)
|
||||
NodeStatusIcons(
|
||||
isThisNode = isThisNode,
|
||||
isFavorite = isFavorite,
|
||||
isUnmessageable = unmessageable,
|
||||
isConnected = isConnected,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
MaterialBatteryInfo(level = thatNode.batteryLevel, voltage = thatNode.voltage)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (distance != null) {
|
||||
DistanceInfo(distance = distance)
|
||||
}
|
||||
thatNode.validPosition?.let { position ->
|
||||
ElevationInfo(
|
||||
altitude = position.altitude,
|
||||
system = system,
|
||||
suffix = stringResource(id = R.string.elevation_suffix),
|
||||
)
|
||||
val satCount = position.satsInView
|
||||
if (satCount > 0) {
|
||||
SatelliteCountInfo(satCount = satCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SignalInfo(node = thatNode, isThisNode = isThisNode)
|
||||
}
|
||||
val telemetryStrings = thatNode.getTelemetryStrings(tempInFahrenheit)
|
||||
|
||||
if (telemetryStrings.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
telemetryStrings.forEach { telemetryString ->
|
||||
Text(
|
||||
text = telemetryString,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val labelStyle =
|
||||
if (thatNode.isUnknownUser) {
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
MaterialTheme.typography.labelSmall.copy(color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
Text(text = thatNode.user.hwModel.name, style = labelStyle)
|
||||
Text(text = thatNode.user.role.name, style = labelStyle)
|
||||
Text(text = thatNode.user.id.ifEmpty { "???" }, style = labelStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoSimplePreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode = NodePreviewParameterProvider().values.last()
|
||||
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, currentTimeMillis = System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatNode: Node) {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
NodeItem(
|
||||
thisNode = thisNode,
|
||||
thatNode = thatNode,
|
||||
distanceUnits = 1,
|
||||
tempInFahrenheit = true,
|
||||
currentTimeMillis = System.currentTimeMillis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,176 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyOff
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.protobuf.ByteString
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.CopyIconButton
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
|
||||
@Composable
|
||||
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) =
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item {
|
||||
Text(text = stringResource(id = title), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(text = stringResource(id = text), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (key != null && title == R.string.encryption_pkc) {
|
||||
val keyString = Base64.encodeToString(key.toByteArray(), Base64.NO_WRAP)
|
||||
Text(
|
||||
text = stringResource(id = R.string.config_security_public_key) + ":",
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SelectionContainer { Text(text = keyString, textAlign = TextAlign.Center) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
CopyIconButton(valueToCopy = keyString, modifier = Modifier.padding(start = 8.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.close))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NodeKeyStatusIcon(
|
||||
hasPKC: Boolean,
|
||||
mismatchKey: Boolean,
|
||||
publicKey: ByteString? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showEncryptionDialog by remember { mutableStateOf(false) }
|
||||
if (showEncryptionDialog) {
|
||||
val (title, text) =
|
||||
when {
|
||||
mismatchKey -> R.string.encryption_error to R.string.encryption_error_text
|
||||
hasPKC -> R.string.encryption_pkc to R.string.encryption_pkc_text
|
||||
else -> R.string.encryption_psk to R.string.encryption_psk_text
|
||||
}
|
||||
KeyStatusDialog(title, text, publicKey) { showEncryptionDialog = false }
|
||||
}
|
||||
|
||||
val (icon, tint) =
|
||||
when {
|
||||
mismatchKey -> Icons.Default.KeyOff to colorScheme.StatusRed
|
||||
hasPKC -> Icons.Default.Lock to colorScheme.StatusGreen
|
||||
else ->
|
||||
ImageVector.vectorResource(com.geeksville.mesh.R.drawable.ic_lock_open_right_24) to
|
||||
colorScheme.StatusYellow
|
||||
}
|
||||
|
||||
IconButton(onClick = { showEncryptionDialog = true }, modifier = modifier) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription =
|
||||
stringResource(
|
||||
id =
|
||||
when {
|
||||
mismatchKey -> R.string.encryption_error
|
||||
hasPKC -> R.string.encryption_pkc
|
||||
else -> R.string.encryption_psk
|
||||
},
|
||||
),
|
||||
tint = tint,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun KeyStatusDialogErrorPreview() {
|
||||
AppTheme { KeyStatusDialog(title = R.string.encryption_error, text = R.string.encryption_error_text, key = null) }
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun KeyStatusDialogPkcPreview() {
|
||||
AppTheme {
|
||||
KeyStatusDialog(
|
||||
title = R.string.encryption_pkc,
|
||||
text = R.string.encryption_pkc_text,
|
||||
key = Channel.getRandomKey(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun KeyStatusDialogPskPreview() {
|
||||
AppTheme { KeyStatusDialog(title = R.string.encryption_psk, text = R.string.encryption_psk_text, key = null) }
|
||||
}
|
||||
|
|
@ -1,98 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
|
||||
@Composable
|
||||
fun NodeActionDialogs(
|
||||
node: Node,
|
||||
displayFavoriteDialog: Boolean,
|
||||
displayIgnoreDialog: Boolean,
|
||||
displayRemoveDialog: Boolean,
|
||||
onDismissMenuRequest: () -> Unit,
|
||||
onConfirmFavorite: (Node) -> Unit,
|
||||
onConfirmIgnore: (Node) -> Unit,
|
||||
onConfirmRemove: (Node) -> Unit,
|
||||
) {
|
||||
if (displayFavoriteDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = R.string.favorite,
|
||||
text =
|
||||
stringResource(
|
||||
id = if (node.isFavorite) R.string.favorite_remove else R.string.favorite_add,
|
||||
node.user.longName,
|
||||
),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmFavorite(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
if (displayIgnoreDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = R.string.ignore,
|
||||
text =
|
||||
stringResource(
|
||||
id = if (node.isIgnored) R.string.ignore_remove else R.string.ignore_add,
|
||||
node.user.longName,
|
||||
),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmIgnore(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
if (displayRemoveDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = R.string.remove,
|
||||
text = R.string.remove_node_text,
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmRemove(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NodeMenuAction {
|
||||
data class Remove(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Ignore(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Favorite(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class DirectMessage(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestUserInfo(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestPosition(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class TraceRoute(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class MoreDetails(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Share(val node: Node) : NodeMenuAction()
|
||||
}
|
||||
|
|
@ -1,128 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.NoCell
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.twotone.CloudDone
|
||||
import androidx.compose.material.icons.twotone.CloudOff
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NodeStatusIcons(isThisNode: Boolean, isUnmessageable: Boolean, isFavorite: Boolean, isConnected: Boolean) {
|
||||
Row(modifier = Modifier.padding(4.dp)) {
|
||||
if (isThisNode) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(
|
||||
stringResource(
|
||||
if (isConnected) {
|
||||
R.string.connected
|
||||
} else {
|
||||
R.string.disconnected
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
if (isConnected) {
|
||||
@Suppress("MagicNumber")
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.CloudDone,
|
||||
contentDescription = stringResource(R.string.connected),
|
||||
modifier = Modifier.size(24.dp), // Smaller size for badge
|
||||
tint = MaterialTheme.colorScheme.StatusGreen,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.CloudOff,
|
||||
contentDescription = stringResource(R.string.not_connected),
|
||||
modifier = Modifier.size(24.dp), // Smaller size for badge
|
||||
tint = MaterialTheme.colorScheme.StatusRed,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnmessageable) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
tooltip = { PlainTooltip { Text(stringResource(R.string.unmonitored_or_infrastructure)) } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.NoCell,
|
||||
contentDescription = stringResource(R.string.unmessageable),
|
||||
modifier = Modifier.size(24.dp), // Smaller size for badge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isFavorite && !isThisNode) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
tooltip = { PlainTooltip { Text(stringResource(R.string.favorite)) } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(24.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = stringResource(R.string.favorite),
|
||||
modifier = Modifier.size(24.dp), // Smaller size for badge
|
||||
tint = MaterialTheme.colorScheme.StatusYellow,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun StatusIconsPreview() {
|
||||
NodeStatusIcons(isThisNode = true, isUnmessageable = true, isFavorite = true, isConnected = false)
|
||||
}
|
||||
|
|
@ -1,42 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.SatelliteAlt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun SatelliteCountInfo(modifier: Modifier = Modifier, satCount: Int) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = Icons.TwoTone.SatelliteAlt,
|
||||
contentDescription = stringResource(org.meshtastic.core.strings.R.string.sats),
|
||||
text = "$satCount",
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun SatelliteCountInfoPreview() {
|
||||
AppTheme { SatelliteCountInfo(satCount = 5) }
|
||||
}
|
||||
|
|
@ -1,100 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.node.components
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Route
|
||||
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
private const val COOL_DOWN_TIME_MS = 30000L
|
||||
|
||||
@Composable
|
||||
fun TracerouteButton(
|
||||
text: String = stringResource(id = R.string.traceroute),
|
||||
lastTracerouteTime: Long?,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val progress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(lastTracerouteTime) {
|
||||
val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0)
|
||||
if (timeSinceLast < COOL_DOWN_TIME_MS) {
|
||||
val remainingTime = COOL_DOWN_TIME_MS - timeSinceLast
|
||||
progress.snapTo(remainingTime / COOL_DOWN_TIME_MS.toFloat())
|
||||
progress.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TracerouteButton(text = text, progress = progress.value, onClick = onClick)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit) {
|
||||
val isCoolingDown = progress > 0f
|
||||
|
||||
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
|
||||
|
||||
SettingsItem(
|
||||
text = text,
|
||||
enabled = !isCoolingDown,
|
||||
leadingIcon = Icons.Default.Route,
|
||||
trailingContent = {
|
||||
if (isCoolingDown) {
|
||||
CircularWavyProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.size(24.dp),
|
||||
stroke = stroke,
|
||||
trackStroke = stroke,
|
||||
wavelength = 8.dp,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (!isCoolingDown) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun TracerouteButtonPreview() {
|
||||
AppTheme { TracerouteButton(text = "Traceroute", progress = .6f, onClick = {}) }
|
||||
}
|
||||
|
|
@ -60,9 +60,6 @@ import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
|||
import com.geeksville.mesh.android.gpsDisabled
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
|
||||
|
|
@ -75,6 +72,9 @@ import kotlinx.coroutines.delay
|
|||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
|
||||
import org.meshtastic.core.ui.component.SettingsItem
|
||||
import org.meshtastic.core.ui.component.SettingsItemDetail
|
||||
import org.meshtastic.core.ui.component.SettingsItemSwitch
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import java.text.SimpleDateFormat
|
||||
|
|
|
|||
|
|
@ -1,185 +0,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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.rounded.Android
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
/** A clickable settings button item. */
|
||||
@Composable
|
||||
fun SettingsItem(
|
||||
text: String,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: ImageVector? = null,
|
||||
leadingIconTint: Color = LocalContentColor.current,
|
||||
trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
|
||||
trailingIconTint: Color = LocalContentColor.current,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
SettingsItem(
|
||||
text = text,
|
||||
textColor = textColor,
|
||||
enabled = enabled,
|
||||
leadingIcon = leadingIcon,
|
||||
leadingIconTint = leadingIconTint,
|
||||
trailingContent = { trailingIcon.Icon(trailingIconTint) },
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
/** A clickable settings button item. */
|
||||
@Composable
|
||||
fun SettingsItem(
|
||||
text: String,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: ImageVector? = null,
|
||||
leadingIconTint: Color = LocalContentColor.current,
|
||||
trailingContent: @Composable (() -> Unit),
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ClickableWrapper(enabled = enabled, onClick = onClick) {
|
||||
Content(
|
||||
leading = { leadingIcon.Icon(leadingIconTint) },
|
||||
text = text,
|
||||
textColor = textColor,
|
||||
trailing = trailingContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A toggleable settings switch item. */
|
||||
@Composable
|
||||
fun SettingsItemSwitch(
|
||||
checked: Boolean,
|
||||
text: String,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: ImageVector? = null,
|
||||
leadingIconTint: Color = LocalContentColor.current,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ClickableWrapper(enabled = enabled, onClick = onClick) {
|
||||
Content(
|
||||
leading = { leadingIcon.Icon(leadingIconTint) },
|
||||
text = text,
|
||||
textColor = textColor,
|
||||
trailing = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A settings detail item. */
|
||||
@Composable
|
||||
fun SettingsItemDetail(
|
||||
text: String,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
icon: ImageVector? = null,
|
||||
iconTint: Color = LocalContentColor.current,
|
||||
trailingText: String? = null,
|
||||
enabled: Boolean = true,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val content: @Composable ColumnScope.() -> Unit = {
|
||||
Content(
|
||||
leading = { icon.Icon(iconTint) },
|
||||
text = text,
|
||||
textColor = textColor,
|
||||
trailing = { trailingText?.let { Text(text = it) } },
|
||||
)
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
ClickableWrapper(enabled = enabled, onClick = onClick, content = content)
|
||||
} else {
|
||||
Column(content = content)
|
||||
}
|
||||
}
|
||||
|
||||
/** A clickable Card wrapper used for all clickable settings items. */
|
||||
@Composable
|
||||
private fun ClickableWrapper(enabled: Boolean, onClick: () -> Unit, content: @Composable ColumnScope.() -> Unit) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
colors =
|
||||
CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/** The row content to display for a settings item. */
|
||||
@Composable
|
||||
private fun Content(leading: @Composable () -> Unit, text: String, textColor: Color, trailing: @Composable () -> Unit) {
|
||||
ListItem(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
headlineContent = { Text(text = text, color = textColor) },
|
||||
leadingContent = { leading() },
|
||||
trailingContent = { trailing() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageVector?.Icon(tint: Color = LocalContentColor.current) =
|
||||
this?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) }
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SettingsItemPreview() {
|
||||
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SettingsItemDisabledPreview() {
|
||||
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SettingsItemSwitchPreview() {
|
||||
AppTheme { SettingsItemSwitch(text = "Text", leadingIcon = Icons.Rounded.Android, checked = true) {} }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SettingsItemDetailPreview() {
|
||||
AppTheme { SettingsItemDetail(text = "Text 1", icon = Icons.Rounded.Android, trailingText = "Text2") }
|
||||
}
|
||||
|
|
@ -46,9 +46,9 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
|
||||
/**
|
||||
* Composable screen for cleaning the node database. Allows users to specify criteria for deleting nodes. The list of
|
||||
|
|
|
|||
|
|
@ -44,11 +44,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.navigation.ConfigRoute
|
||||
import com.geeksville.mesh.navigation.ModuleRoute
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.radio.components.WarningDialog
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItem
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
|
|
|||
|
|
@ -1,29 +0,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/>.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12 7.5C12.69 7.5 13.27 7.73 13.76 8.2S14.5 9.27 14.5 10C14.5 11.05 14 11.81 13 12.28V21H11V12.28C10 11.81 9.5 11.05 9.5 10C9.5 9.27 9.76 8.67 10.24 8.2S11.31 7.5 12 7.5M16.69 5.3C17.94 6.55 18.61 8.11 18.7 10C18.7 11.8 18.03 13.38 16.69 14.72L15.5 13.5C16.5 12.59 17 11.42 17 10C17 8.67 16.5 7.5 15.5 6.5L16.69 5.3M6.09 4.08C4.5 5.67 3.7 7.64 3.7 10S4.5 14.3 6.09 15.89L4.92 17.11C3 15.08 2 12.7 2 10C2 7.3 3 4.94 4.92 2.91L6.09 4.08M19.08 2.91C21 4.94 22 7.3 22 10C22 12.8 21 15.17 19.08 17.11L17.91 15.89C19.5 14.3 20.3 12.33 20.3 10S19.5 5.67 17.91 4.08L19.08 2.91M7.31 5.3L8.5 6.5C7.5 7.42 7 8.58 7 10C7 11.33 7.5 12.5 8.5 13.5L7.31 14.72C5.97 13.38 5.3 11.8 5.3 10C5.3 8.2 5.97 6.64 7.31 5.3Z"
|
||||
android:fillAlpha=".5"
|
||||
/>
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,800h480v-400L240,400v400ZM480,680q33,0 56.5,-23.5T560,600q0,-33 -23.5,-56.5T480,520q-33,0 -56.5,23.5T400,600q0,33 23.5,56.5T480,680ZM240,800v-400,400ZM240,880q-33,0 -56.5,-23.5T160,800v-400q0,-33 23.5,-56.5T240,320h280v-80q0,-83 58.5,-141.5T720,40q83,0 141.5,58.5T920,240h-80q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85v80h120q33,0 56.5,23.5T800,400v400q0,33 -23.5,56.5T720,880L240,880Z" />
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue