mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix(release): fixes to prep for release (#4546)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
c5f2b1bbea
commit
80d9a2e0aa
30 changed files with 2324 additions and 312 deletions
|
|
@ -162,7 +162,7 @@ constructor(
|
|||
val spinner: StateFlow<Boolean> = bluetoothRepository.isScanning
|
||||
|
||||
init {
|
||||
serviceRepository.statusMessage.onEach { errorText.value = it }.launchIn(viewModelScope)
|
||||
serviceRepository.connectionProgress.onEach { errorText.value = it }.launchIn(viewModelScope)
|
||||
Logger.d { "BTScanModel created" }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ constructor(
|
|||
metadata != null -> router.configFlowManager.handleLocalMetadata(metadata)
|
||||
nodeInfo != null -> {
|
||||
router.configFlowManager.handleNodeInfo(nodeInfo)
|
||||
serviceRepository.setStatusMessage("Nodes (${router.configFlowManager.newNodeCount})")
|
||||
serviceRepository.setConnectionProgress("Nodes (${router.configFlowManager.newNodeCount})")
|
||||
}
|
||||
configCompleteId != null -> router.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ constructor(
|
|||
fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val u = User.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) }
|
||||
nodeManager.handleReceivedUser(destNum, u)
|
||||
}
|
||||
|
||||
fun handleGetRemoteOwner(id: Int, destNum: Int) {
|
||||
|
|
@ -237,6 +238,7 @@ constructor(
|
|||
fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val c = ModuleConfig.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) }
|
||||
c.statusmessage?.node_status?.let { status -> nodeManager.updateNodeStatus(destNum, status) }
|
||||
}
|
||||
|
||||
fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) {
|
||||
|
|
|
|||
|
|
@ -59,12 +59,16 @@ constructor(
|
|||
|
||||
fun handleDeviceConfig(config: Config) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalConfig(config) }
|
||||
serviceRepository.setStatusMessage("Device config received")
|
||||
serviceRepository.setConnectionProgress("Device config received")
|
||||
}
|
||||
|
||||
fun handleModuleConfig(config: ModuleConfig) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) }
|
||||
serviceRepository.setStatusMessage("Module config received")
|
||||
serviceRepository.setConnectionProgress("Module config received")
|
||||
|
||||
config.statusmessage?.node_status?.let { status ->
|
||||
nodeManager.myNodeNum?.let { num -> nodeManager.updateNodeStatus(num, status) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleChannel(ch: Channel) {
|
||||
|
|
@ -75,9 +79,9 @@ constructor(
|
|||
val mi = nodeManager.getMyNodeInfo()
|
||||
val index = ch.index ?: 0
|
||||
if (mi != null) {
|
||||
serviceRepository.setStatusMessage("Channels (${index + 1} / ${mi.maxChannels})")
|
||||
serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})")
|
||||
} else {
|
||||
serviceRepository.setStatusMessage("Channels (${index + 1})")
|
||||
serviceRepository.setConnectionProgress("Channels (${index + 1})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,14 @@ constructor(
|
|||
u.session_passkey.let { commandSender.setSessionPasskey(it) }
|
||||
|
||||
val fromNum = packet.from
|
||||
u.get_module_config_response?.let { config ->
|
||||
if (fromNum == myNodeNum) {
|
||||
configHandler.handleModuleConfig(config)
|
||||
} else {
|
||||
config.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) }
|
||||
}
|
||||
}
|
||||
|
||||
if (fromNum == myNodeNum) {
|
||||
u.get_config_response?.let { configHandler.handleDeviceConfig(it) }
|
||||
u.get_channel_response?.let { configHandler.handleChannel(it) }
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManage
|
|||
relayNode = packet.relay_node,
|
||||
viaMqtt = packet.via_mqtt == true,
|
||||
emoji = decoded.emoji,
|
||||
transportMechanism = packet.transport_mechanism.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.util.isLora
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.LogRecord
|
||||
|
|
@ -205,11 +206,20 @@ constructor(
|
|||
}
|
||||
nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) {
|
||||
it.lastHeard = packet.rx_time
|
||||
it.snr = packet.rx_snr
|
||||
it.rssi = packet.rx_rssi
|
||||
it.viaMqtt = packet.via_mqtt == true
|
||||
it.lastTransport = packet.transport_mechanism.value
|
||||
|
||||
val isDirect = packet.hop_start == packet.hop_limit
|
||||
if (isDirect && packet.isLora() && !it.viaMqtt) {
|
||||
it.snr = packet.rx_snr
|
||||
it.rssi = packet.rx_rssi
|
||||
}
|
||||
|
||||
it.hopsAway =
|
||||
if (decoded.portnum == PortNum.RANGE_TEST_APP) {
|
||||
0
|
||||
} else if (it.viaMqtt) {
|
||||
-1
|
||||
} else if (packet.hop_start == 0 && (decoded.bitfield ?: 0) == 0) {
|
||||
-1
|
||||
} else if (packet.hop_limit > packet.hop_start) {
|
||||
|
|
|
|||
|
|
@ -214,7 +214,11 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) {
|
||||
updateNodeInfo(fromNum) { it.nodeStatus = s.status }
|
||||
updateNodeStatus(fromNum, s.status)
|
||||
}
|
||||
|
||||
fun updateNodeStatus(nodeNum: Int, status: String) {
|
||||
updateNodeInfo(nodeNum) { it.nodeStatus = status }
|
||||
}
|
||||
|
||||
fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class FromRadioPacketHandlerTest {
|
|||
handler.handleFromRadio(proto)
|
||||
|
||||
verify { router.configFlowManager.handleNodeInfo(nodeInfo) }
|
||||
verify { serviceRepository.setStatusMessage(any()) }
|
||||
verify { serviceRepository.setConnectionProgress(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -93,8 +93,10 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
|
|||
AutoMigration(from = 32, to = 33),
|
||||
AutoMigration(from = 33, to = 34, spec = AutoMigration33to34::class),
|
||||
AutoMigration(from = 34, to = 35, spec = AutoMigration34to35::class),
|
||||
AutoMigration(from = 35, to = 36),
|
||||
AutoMigration(from = 36, to = 37),
|
||||
],
|
||||
version = 35,
|
||||
version = 37,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import org.meshtastic.core.model.Position
|
|||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
|
|
@ -65,6 +66,7 @@ data class NodeWithRelations(
|
|||
notes = notes,
|
||||
manuallyVerified = manuallyVerified,
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +91,7 @@ data class NodeWithRelations(
|
|||
notes = notes,
|
||||
manuallyVerified = manuallyVerified,
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +143,8 @@ data class NodeEntity(
|
|||
@ColumnInfo(name = "manually_verified", defaultValue = "0")
|
||||
var manuallyVerified: Boolean = false, // ONLY set true when scanned/imported manually
|
||||
@ColumnInfo(name = "node_status") var nodeStatus: String? = null,
|
||||
/** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */
|
||||
@ColumnInfo(name = "last_transport", defaultValue = "0") var lastTransport: Int = 0,
|
||||
) {
|
||||
val deviceMetrics: org.meshtastic.proto.DeviceMetrics?
|
||||
get() = deviceTelemetry.device_metrics
|
||||
|
|
@ -199,6 +204,7 @@ data class NodeEntity(
|
|||
publicKey = publicKey ?: user.public_key,
|
||||
notes = notes,
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
|
||||
fun toNodeInfo() = NodeInfo(
|
||||
|
|
@ -243,5 +249,6 @@ data class NodeEntity(
|
|||
environmentTelemetry.time,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
nodeStatus = nodeStatus,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ data class PacketEntity(
|
|||
relayNode = data.relayNode,
|
||||
relays = data.relays,
|
||||
filtered = filtered,
|
||||
transportMechanism = data.transportMechanism,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import org.meshtastic.core.strings.routing_error_rate_limit_exceeded
|
|||
import org.meshtastic.core.strings.routing_error_timeout
|
||||
import org.meshtastic.core.strings.routing_error_too_large
|
||||
import org.meshtastic.core.strings.unrecognized
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
|
@ -92,6 +93,8 @@ data class Message(
|
|||
val relayNode: Int? = null,
|
||||
val relays: Int = 0,
|
||||
val filtered: Boolean = false,
|
||||
/** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */
|
||||
val transportMechanism: Int = 0,
|
||||
) {
|
||||
fun getStatusStringRes(): Pair<StringResource, StringResource> {
|
||||
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import org.meshtastic.proto.DeviceMetadata
|
|||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.PowerMetrics
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -57,6 +59,8 @@ data class Node(
|
|||
val notes: String = "",
|
||||
val manuallyVerified: Boolean = false,
|
||||
val nodeStatus: String? = null,
|
||||
/** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */
|
||||
val lastTransport: Int = 0,
|
||||
) {
|
||||
val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) }
|
||||
|
||||
|
|
@ -175,6 +179,32 @@ data class Node(
|
|||
|
||||
fun getTelemetryStrings(isFahrenheit: Boolean = false): List<String> =
|
||||
environmentMetrics.getDisplayStrings(isFahrenheit)
|
||||
|
||||
fun toEntity() = NodeEntity(
|
||||
num = num,
|
||||
user = user,
|
||||
position = position,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceTelemetry = Telemetry(device_metrics = deviceMetrics),
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
isMuted = isMuted,
|
||||
environmentTelemetry = Telemetry(environment_metrics = environmentMetrics),
|
||||
powerTelemetry = Telemetry(power_metrics = powerMetrics),
|
||||
paxcounter = paxcounter,
|
||||
publicKey = publicKey ?: user.public_key,
|
||||
notes = notes,
|
||||
manuallyVerified = manuallyVerified,
|
||||
nodeStatus = nodeStatus,
|
||||
lastTransport = lastTransport,
|
||||
)
|
||||
}
|
||||
|
||||
fun Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import okio.ByteString
|
|||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.util.ByteStringParceler
|
||||
import org.meshtastic.core.model.util.ByteStringSerializer
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Waypoint
|
||||
|
||||
|
|
@ -70,6 +71,8 @@ data class DataPacket(
|
|||
@Serializable(with = ByteStringSerializer::class)
|
||||
@TypeParceler<ByteString?, ByteStringParceler>
|
||||
var sfppHash: ByteString? = null,
|
||||
/** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */
|
||||
var transportMechanism: Int = 0,
|
||||
) : Parcelable {
|
||||
|
||||
fun readFromParcel(parcel: Parcel) {
|
||||
|
|
@ -108,6 +111,7 @@ data class DataPacket(
|
|||
viaMqtt = parcel.readInt() != 0
|
||||
emoji = parcel.readInt()
|
||||
sfppHash = ByteStringParceler.create(parcel)
|
||||
transportMechanism = parcel.readInt()
|
||||
}
|
||||
|
||||
/** If there was an error with this message, this string describes what was wrong. */
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ data class NodeInfo(
|
|||
var channel: Int = 0,
|
||||
var environmentMetrics: EnvironmentMetrics? = null,
|
||||
var hopsAway: Int = 0,
|
||||
var nodeStatus: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
|||
|
|
@ -68,3 +68,9 @@ fun Int.mpsToMph(): Float {
|
|||
val mph = this * MPS_TO_KMPH * KM_TO_MILES
|
||||
return mph
|
||||
}
|
||||
|
||||
/** Returns true if this packet arrived via a LoRa transport mechanism. */
|
||||
fun MeshPacket.isLora(): Boolean = transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA ||
|
||||
transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT1 ||
|
||||
transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT2 ||
|
||||
transport_mechanism == MeshPacket.TransportMechanism.TRANSPORT_LORA_ALT3
|
||||
|
|
|
|||
|
|
@ -87,13 +87,13 @@ class ServiceRepository @Inject constructor() {
|
|||
_errorMessage.value = null
|
||||
}
|
||||
|
||||
private val _statusMessage = MutableStateFlow<String?>(null)
|
||||
val statusMessage: StateFlow<String?>
|
||||
get() = _statusMessage
|
||||
private val _connectionProgress = MutableStateFlow<String?>(null)
|
||||
val connectionProgress: StateFlow<String?>
|
||||
get() = _connectionProgress
|
||||
|
||||
fun setStatusMessage(text: String) {
|
||||
fun setConnectionProgress(text: String) {
|
||||
if (connectionState.value != ConnectionState.Connected) {
|
||||
_statusMessage.value = text
|
||||
_connectionProgress.value = text
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@
|
|||
<string name="node_sort_last_heard">Last heard</string>
|
||||
<string name="node_sort_via_mqtt">via MQTT</string>
|
||||
<string name="via_mqtt">via MQTT</string>
|
||||
<string name="via_udp">via UDP</string>
|
||||
<string name="via_api">via API</string>
|
||||
<string name="internal">Internal</string>
|
||||
<string name="node_sort_via_favorite">via Favorite</string>
|
||||
<string name="node_filter_show_ignored">Only show ignored Nodes</string>
|
||||
<string name="unrecognized">Unrecognized</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.internal
|
||||
import org.meshtastic.core.strings.via_api
|
||||
import org.meshtastic.core.strings.via_mqtt
|
||||
import org.meshtastic.core.strings.via_udp
|
||||
import org.meshtastic.core.ui.icon.Api
|
||||
import org.meshtastic.core.ui.icon.Cloud
|
||||
import org.meshtastic.core.ui.icon.Device
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Udp
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
|
||||
@Composable
|
||||
fun TransportIcon(transport: Int, viaMqtt: Boolean, modifier: Modifier = Modifier) {
|
||||
val (icon, description) =
|
||||
when {
|
||||
viaMqtt || transport == MeshPacket.TransportMechanism.TRANSPORT_MQTT.value ->
|
||||
MeshtasticIcons.Cloud to stringResource(Res.string.via_mqtt)
|
||||
transport == MeshPacket.TransportMechanism.TRANSPORT_MULTICAST_UDP.value ->
|
||||
MeshtasticIcons.Udp to stringResource(Res.string.via_udp)
|
||||
transport == MeshPacket.TransportMechanism.TRANSPORT_API.value ->
|
||||
MeshtasticIcons.Api to stringResource(Res.string.via_api)
|
||||
transport == MeshPacket.TransportMechanism.TRANSPORT_INTERNAL.value ->
|
||||
MeshtasticIcons.Device to stringResource(Res.string.internal)
|
||||
else -> return
|
||||
}
|
||||
Icon(icon, contentDescription = description, modifier = modifier)
|
||||
}
|
||||
|
|
@ -28,10 +28,13 @@ import androidx.compose.material.icons.rounded.Cloud
|
|||
import androidx.compose.material.icons.rounded.CloudOff
|
||||
import androidx.compose.material.icons.rounded.Dangerous
|
||||
import androidx.compose.material.icons.rounded.History
|
||||
import androidx.compose.material.icons.rounded.Lan
|
||||
import androidx.compose.material.icons.rounded.NoCell
|
||||
import androidx.compose.material.icons.rounded.SettingsEthernet
|
||||
import androidx.compose.material.icons.rounded.SpeakerNotesOff
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarBorder
|
||||
import androidx.compose.material.icons.rounded.Terminal
|
||||
import androidx.compose.material.icons.twotone.Cloud
|
||||
import androidx.compose.material.icons.twotone.CloudDone
|
||||
import androidx.compose.material.icons.twotone.CloudOff
|
||||
|
|
@ -84,3 +87,10 @@ val MeshtasticIcons.CheckCircle: ImageVector
|
|||
|
||||
val MeshtasticIcons.Acknowledged: ImageVector
|
||||
get() = Icons.TwoTone.HowToReg
|
||||
|
||||
val MeshtasticIcons.Udp: ImageVector
|
||||
get() = Icons.Rounded.Lan
|
||||
val MeshtasticIcons.Api: ImageVector
|
||||
get() = Icons.Rounded.Terminal
|
||||
val MeshtasticIcons.Ethernet: ImageVector
|
||||
get() = Icons.Rounded.SettingsEthernet
|
||||
|
|
|
|||
|
|
@ -70,15 +70,14 @@ import org.meshtastic.core.strings.filter_message_label
|
|||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.sample_message
|
||||
import org.meshtastic.core.strings.via_mqtt
|
||||
import org.meshtastic.core.ui.component.AutoLinkText
|
||||
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.TransportIcon
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.emoji.EmojiPicker
|
||||
import org.meshtastic.core.ui.icon.Acknowledged
|
||||
import org.meshtastic.core.ui.icon.Cloud
|
||||
import org.meshtastic.core.ui.icon.CloudDone
|
||||
import org.meshtastic.core.ui.icon.CloudOffTwoTone
|
||||
import org.meshtastic.core.ui.icon.CloudSync
|
||||
|
|
@ -239,13 +238,11 @@ internal fun MessageItem(
|
|||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
if (message.viaMqtt) {
|
||||
Icon(
|
||||
MeshtasticIcons.Cloud,
|
||||
contentDescription = stringResource(Res.string.via_mqtt),
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
TransportIcon(
|
||||
transport = message.transportMechanism,
|
||||
viaMqtt = message.viaMqtt,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
|
|
@ -292,7 +289,7 @@ internal fun MessageItem(
|
|||
|
||||
Row(modifier = Modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
if (!message.fromLocal) {
|
||||
if (message.hopsAway == 0) {
|
||||
if (message.hopsAway == 0 && !message.viaMqtt) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Snr(message.snr)
|
||||
Rssi(message.rssi)
|
||||
|
|
@ -309,7 +306,12 @@ internal fun MessageItem(
|
|||
tint = cardColors.contentColor.copy(alpha = 0.7f),
|
||||
)
|
||||
Text(
|
||||
text = message.hopsAway.toString(),
|
||||
text =
|
||||
if (message.hopsAway >= 0) {
|
||||
message.hopsAway.toString()
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
|
|||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SectionDivider()
|
||||
|
||||
val deviceText =
|
||||
state.reportedTarget?.let { target -> "${deviceHardware.displayName} ($target)" }
|
||||
?: deviceHardware.displayName
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
|
|
@ -30,6 +32,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Notes
|
||||
import androidx.compose.material.icons.rounded.Numbers
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -48,6 +51,7 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -69,10 +73,12 @@ import org.meshtastic.core.strings.role
|
|||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.short_name
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.strings.status_message
|
||||
import org.meshtastic.core.strings.supported
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.user_id
|
||||
import org.meshtastic.core.strings.via_mqtt
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.ArrowCircleUp
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
import org.meshtastic.core.ui.icon.Cloud
|
||||
|
|
@ -84,6 +90,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
|
|||
import org.meshtastic.core.ui.icon.Person
|
||||
import org.meshtastic.core.ui.icon.Role
|
||||
import org.meshtastic.core.ui.icon.Verified
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
||||
@Composable
|
||||
|
|
@ -135,13 +142,17 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) {
|
|||
private fun MainNodeDetails(node: Node) {
|
||||
Column {
|
||||
NameAndRoleRow(node)
|
||||
node.nodeStatus?.let { status ->
|
||||
SectionDivider()
|
||||
StatusMessageRow(status)
|
||||
}
|
||||
SectionDivider()
|
||||
NodeIdentificationRow(node)
|
||||
SectionDivider()
|
||||
HearsAndHopsRow(node)
|
||||
SectionDivider()
|
||||
UserAndUptimeRow(node)
|
||||
if (node.hopsAway == 0) {
|
||||
if (node.hopsAway == 0 && !node.viaMqtt) {
|
||||
SectionDivider()
|
||||
SignalRow(node)
|
||||
}
|
||||
|
|
@ -175,6 +186,16 @@ private fun NameAndRoleRow(node: Node) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusMessageRow(status: String) {
|
||||
InfoItem(
|
||||
label = stringResource(Res.string.status_message),
|
||||
value = status,
|
||||
icon = Icons.AutoMirrored.Rounded.Notes,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NodeIdentificationRow(node: Node) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
|
|
@ -352,3 +373,12 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun NodeDetailsSectionPreview() {
|
||||
AppTheme {
|
||||
val node = NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.")
|
||||
NodeDetailsSection(node = node)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.Notes
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
|
|
@ -84,6 +87,7 @@ import org.meshtastic.core.ui.component.Snr
|
|||
import org.meshtastic.core.ui.component.SoilMoistureInfo
|
||||
import org.meshtastic.core.ui.component.SoilTemperatureInfo
|
||||
import org.meshtastic.core.ui.component.TemperatureInfo
|
||||
import org.meshtastic.core.ui.component.TransportIcon
|
||||
import org.meshtastic.core.ui.component.determineSignalQuality
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.AirUtilization
|
||||
|
|
@ -171,6 +175,28 @@ fun NodeItem(
|
|||
contentColor = contentColor,
|
||||
)
|
||||
|
||||
thatNode.nodeStatus?.let { status ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.Notes,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = contentColor.copy(alpha = 0.7f),
|
||||
)
|
||||
Text(
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = contentColor,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NodeBatteryPositionRow(
|
||||
thatNode = thatNode,
|
||||
distance = distance,
|
||||
|
|
@ -252,7 +278,7 @@ private fun NodeSignalRow(thatNode: Node, isThisNode: Boolean, contentColor: Col
|
|||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (thatNode.hopsAway > 0) {
|
||||
HopsInfo(hops = thatNode.hopsAway, contentColor = contentColor)
|
||||
} else {
|
||||
} else if (thatNode.hopsAway == 0 && !thatNode.viaMqtt) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -395,13 +421,21 @@ private fun NodeItemHeader(
|
|||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = longName,
|
||||
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = longName,
|
||||
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
TransportIcon(
|
||||
transport = thatNode.lastTransport,
|
||||
viaMqtt = thatNode.viaMqtt,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
LastHeardInfo(lastHeard = thatNode.lastHeard, showLabel = false, contentColor = contentColor)
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +473,17 @@ fun NodeInfoSimplePreview() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoStatusPreview() {
|
||||
AppTheme {
|
||||
val thisNode = NodePreviewParameterProvider().values.first()
|
||||
val thatNode =
|
||||
NodePreviewParameterProvider().values.last().copy(nodeStatus = "Going to the farm.. to grow wheat.")
|
||||
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, connectionState = ConnectionState.Connected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun NodeInfoSignalPreview() {
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.node.detail
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.compass.CompassUiState
|
||||
import org.meshtastic.feature.node.compass.CompassViewModel
|
||||
import org.meshtastic.feature.node.component.AdministrationSection
|
||||
import org.meshtastic.feature.node.component.CompassSheetContent
|
||||
import org.meshtastic.feature.node.component.DeviceActions
|
||||
import org.meshtastic.feature.node.component.DeviceDetailsSection
|
||||
import org.meshtastic.feature.node.component.FirmwareReleaseSheetContent
|
||||
import org.meshtastic.feature.node.component.NodeDetailsSection
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
import org.meshtastic.feature.node.component.NotesSection
|
||||
import org.meshtastic.feature.node.component.PositionSection
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
||||
@Composable
|
||||
fun NodeDetailContent(
|
||||
node: Node,
|
||||
ourNode: Node?,
|
||||
metricsState: MetricsState,
|
||||
lastTracerouteTime: Long?,
|
||||
lastRequestNeighborsTime: Long?,
|
||||
availableLogs: Set<LogsType>,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
onSaveNotes: (nodeNum: Int, notes: String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showShareDialog by remember { mutableStateOf(false) }
|
||||
if (showShareDialog) {
|
||||
SharedContactDialog(node) { showShareDialog = false }
|
||||
}
|
||||
|
||||
NodeDetailList(
|
||||
node = node,
|
||||
lastTracerouteTime = lastTracerouteTime,
|
||||
lastRequestNeighborsTime = lastRequestNeighborsTime,
|
||||
ourNode = ourNode,
|
||||
metricsState = metricsState,
|
||||
onAction = { action ->
|
||||
if (action is NodeDetailAction.ShareContact) {
|
||||
showShareDialog = true
|
||||
} else {
|
||||
onAction(action)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
availableLogs = availableLogs,
|
||||
onSaveNotes = onSaveNotes,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun NodeDetailList(
|
||||
node: Node,
|
||||
lastTracerouteTime: Long?,
|
||||
lastRequestNeighborsTime: Long?,
|
||||
ourNode: Node?,
|
||||
metricsState: MetricsState,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
availableLogs: Set<LogsType>,
|
||||
onSaveNotes: (Int, String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showFirmwareSheet by remember { mutableStateOf(false) }
|
||||
var selectedFirmware by remember { mutableStateOf<FirmwareRelease?>(null) }
|
||||
var showCompassSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val inspectionMode = LocalInspectionMode.current
|
||||
val compassViewModel = if (inspectionMode) null else hiltViewModel<CompassViewModel>()
|
||||
val compassUiState by
|
||||
compassViewModel?.uiState?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(CompassUiState()) }
|
||||
var compassTargetNode by remember { mutableStateOf<Node?>(null) }
|
||||
|
||||
val permissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { _ -> }
|
||||
val locationSettingsLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> }
|
||||
|
||||
FirmwareSheetHost(
|
||||
showFirmwareSheet = showFirmwareSheet,
|
||||
onDismiss = { showFirmwareSheet = false },
|
||||
firmwareRelease = selectedFirmware,
|
||||
)
|
||||
|
||||
CompassSheetHost(
|
||||
showCompassSheet = showCompassSheet,
|
||||
compassViewModel = compassViewModel,
|
||||
compassUiState = compassUiState,
|
||||
onDismiss = { showCompassSheet = false },
|
||||
permissionLauncher = permissionLauncher,
|
||||
locationSettingsLauncher = locationSettingsLauncher,
|
||||
onRequestPosition = {
|
||||
compassTargetNode?.let { target ->
|
||||
onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(target)))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp).focusable(),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
NodeDetailsSection(node)
|
||||
|
||||
DeviceActions(
|
||||
isLocal = metricsState.isLocal,
|
||||
lastTracerouteTime = lastTracerouteTime,
|
||||
lastRequestNeighborsTime = lastRequestNeighborsTime,
|
||||
node = node,
|
||||
availableLogs = availableLogs,
|
||||
onAction = onAction,
|
||||
metricsState = metricsState,
|
||||
)
|
||||
|
||||
PositionSection(
|
||||
node = node,
|
||||
ourNode = ourNode,
|
||||
metricsState = metricsState,
|
||||
availableLogs = availableLogs,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeDetailAction.OpenCompass -> {
|
||||
compassViewModel?.start(action.node, action.displayUnits)
|
||||
compassTargetNode = action.node
|
||||
showCompassSheet = compassViewModel != null
|
||||
}
|
||||
|
||||
else -> onAction(action)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (metricsState.deviceHardware != null) {
|
||||
DeviceDetailsSection(metricsState)
|
||||
}
|
||||
|
||||
NotesSection(node = node, onSaveNotes = onSaveNotes)
|
||||
|
||||
if (!metricsState.isManaged) {
|
||||
AdministrationSection(
|
||||
node = node,
|
||||
metricsState = metricsState,
|
||||
onAction = onAction,
|
||||
onFirmwareSelect = { firmware ->
|
||||
selectedFirmware = firmware
|
||||
showFirmwareSheet = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FirmwareSheetHost(showFirmwareSheet: Boolean, onDismiss: () -> Unit, firmwareRelease: FirmwareRelease?) {
|
||||
if (showFirmwareSheet) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||
firmwareRelease?.let { FirmwareReleaseSheetContent(firmwareRelease = it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
private fun CompassSheetHost(
|
||||
showCompassSheet: Boolean,
|
||||
compassViewModel: CompassViewModel?,
|
||||
compassUiState: CompassUiState,
|
||||
onDismiss: () -> Unit,
|
||||
permissionLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>>,
|
||||
locationSettingsLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||
onRequestPosition: () -> Unit,
|
||||
) {
|
||||
if (showCompassSheet && compassViewModel != null) {
|
||||
DisposableEffect(Unit) { onDispose { compassViewModel.stop() } }
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
compassViewModel.stop()
|
||||
onDismiss()
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
CompassSheetContent(
|
||||
uiState = compassUiState,
|
||||
onRequestLocationPermission = {
|
||||
permissionLauncher.launch(
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
|
||||
)
|
||||
},
|
||||
onOpenLocationSettings = {
|
||||
locationSettingsLauncher.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
},
|
||||
onRequestPosition = onRequestPosition,
|
||||
modifier = Modifier.padding(bottom = 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun NodeDetailsPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
|
||||
AppTheme {
|
||||
NodeDetailList(
|
||||
node = node,
|
||||
ourNode = node,
|
||||
lastTracerouteTime = null,
|
||||
lastRequestNeighborsTime = null,
|
||||
metricsState = MetricsState.Companion.Empty,
|
||||
availableLogs = emptySet(),
|
||||
onAction = {},
|
||||
onSaveNotes = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -61,9 +63,12 @@ import org.meshtastic.core.database.entity.FirmwareRelease
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.details
|
||||
import org.meshtastic.core.strings.loading
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.compass.CompassUiState
|
||||
import org.meshtastic.feature.node.compass.CompassViewModel
|
||||
import org.meshtastic.feature.node.component.AdministrationSection
|
||||
|
|
@ -75,6 +80,7 @@ import org.meshtastic.feature.node.component.NodeDetailsSection
|
|||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
import org.meshtastic.feature.node.component.NotesSection
|
||||
import org.meshtastic.feature.node.component.PositionSection
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
||||
private sealed interface NodeDetailOverlay {
|
||||
|
|
@ -143,7 +149,8 @@ private fun NodeDetailScaffold(
|
|||
modifier = modifier,
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = node?.user?.long_name ?: "",
|
||||
title = getString(Res.string.details),
|
||||
subtitle = node?.user?.long_name ?: "",
|
||||
ourNode = uiState.ourNode,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
@ -343,3 +350,26 @@ private fun handleNodeAction(
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun NodeDetailListPreview(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
|
||||
AppTheme {
|
||||
val uiState =
|
||||
NodeDetailUiState(
|
||||
node = node,
|
||||
ourNode = node,
|
||||
metricsState = MetricsState(node = node, isLocal = true, isManaged = false),
|
||||
availableLogs = emptySet(),
|
||||
)
|
||||
NodeDetailList(
|
||||
node = node,
|
||||
ourNode = node,
|
||||
uiState = uiState,
|
||||
listState = rememberLazyListState(),
|
||||
onAction = {},
|
||||
onFirmwareSelect = {},
|
||||
onSaveNotes = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import org.meshtastic.core.database.entity.MyNodeEntity
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.util.isLora
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
|
|
@ -216,7 +217,13 @@ constructor(
|
|||
deviceMetrics = data.telemetry.filter { it.device_metrics != null },
|
||||
powerMetrics = data.telemetry.filter { it.power_metrics != null },
|
||||
hostMetrics = data.telemetry.filter { it.host_metrics != null },
|
||||
signalMetrics = data.packets.filter { (it.rx_time ?: 0) > 0 },
|
||||
signalMetrics =
|
||||
data.packets.filter { pkt ->
|
||||
(pkt.rx_time ?: 0) > 0 &&
|
||||
pkt.hop_start == pkt.hop_limit &&
|
||||
pkt.via_mqtt != true &&
|
||||
pkt.isLora()
|
||||
},
|
||||
positionLogs = data.positionPackets.mapNotNull { it.toPosition() },
|
||||
paxMetrics = data.paxLogs,
|
||||
tracerouteRequests = data.tracerouteRequests,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue