mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(ui): Refactor node position details into separate section (#3382)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b2ff4483c8
commit
8baf8714d0
42 changed files with 1967 additions and 1193 deletions
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
||||
@Composable
|
||||
internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
// No-op for F-Droid builds
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.android.gms.maps.model.CameraPosition
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.maps.android.compose.Circle
|
||||
import com.google.maps.android.compose.ComposeMapColorScheme
|
||||
import com.google.maps.android.compose.GoogleMap
|
||||
import com.google.maps.android.compose.MapUiSettings
|
||||
import com.google.maps.android.compose.MapsComposeExperimentalApi
|
||||
import com.google.maps.android.compose.MarkerComposable
|
||||
import com.google.maps.android.compose.rememberCameraPositionState
|
||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.component.precisionBitsToMeters
|
||||
|
||||
@OptIn(MapsComposeExperimentalApi::class)
|
||||
@Composable
|
||||
internal fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
val dark = isSystemInDarkTheme()
|
||||
val mapColorScheme =
|
||||
when (dark) {
|
||||
true -> ComposeMapColorScheme.DARK
|
||||
else -> ComposeMapColorScheme.LIGHT
|
||||
}
|
||||
|
||||
val location = LatLng(node.latitude, node.longitude)
|
||||
val cameraState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(location, 15f) }
|
||||
GoogleMap(
|
||||
mapColorScheme = mapColorScheme,
|
||||
modifier = modifier,
|
||||
uiSettings =
|
||||
MapUiSettings(
|
||||
zoomControlsEnabled = true,
|
||||
mapToolbarEnabled = false,
|
||||
compassEnabled = false,
|
||||
myLocationButtonEnabled = false,
|
||||
rotationGesturesEnabled = false,
|
||||
scrollGesturesEnabled = false,
|
||||
tiltGesturesEnabled = false,
|
||||
zoomGesturesEnabled = false,
|
||||
),
|
||||
cameraPositionState = cameraState,
|
||||
) {
|
||||
val precisionMeters = precisionBitsToMeters(node.position.precisionBits)
|
||||
val latLng = LatLng(node.latitude, node.longitude)
|
||||
if (precisionMeters > 0) {
|
||||
Circle(
|
||||
center = latLng,
|
||||
radius = precisionMeters,
|
||||
fillColor = Color(node.colors.second).copy(alpha = 0.2f),
|
||||
strokeColor = Color(node.colors.second),
|
||||
strokeWidth = 2f,
|
||||
)
|
||||
}
|
||||
MarkerComposable(state = rememberUpdatedMarkerState(position = latLng)) { NodeChip(node = node) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ForkLeft
|
||||
import androidx.compose.material.icons.filled.Icecream
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
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.TitledCard
|
||||
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.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun AdministrationSection(
|
||||
node: Node,
|
||||
metricsState: MetricsState,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
onFirmwareSelect: (FirmwareRelease) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TitledCard(stringResource(id = R.string.administration), modifier = modifier) {
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.request_metadata),
|
||||
leadingIcon = Icons.Default.Memory,
|
||||
trailingContent = {},
|
||||
onClick = { onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num))) },
|
||||
)
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.remote_admin),
|
||||
leadingIcon = Icons.Default.Settings,
|
||||
enabled = metricsState.isLocal || node.metadata != null,
|
||||
) {
|
||||
onAction(NodeDetailAction.Navigate(SettingsRoutes.Settings(node.num)))
|
||||
}
|
||||
}
|
||||
val firmwareVersion = node.metadata?.firmwareVersion
|
||||
val firmwareEdition = metricsState.firmwareEdition
|
||||
if (firmwareVersion != null || (firmwareEdition != null && metricsState.isLocal)) {
|
||||
TitledCard(stringResource(R.string.firmware)) {
|
||||
firmwareEdition?.let {
|
||||
val icon =
|
||||
when (it) {
|
||||
MeshProtos.FirmwareEdition.VANILLA -> Icons.Default.Icecream
|
||||
else -> Icons.Default.ForkLeft
|
||||
}
|
||||
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.firmware_edition),
|
||||
icon = icon,
|
||||
supportingText = it.name,
|
||||
)
|
||||
}
|
||||
firmwareVersion?.let { firmwareVersion ->
|
||||
val latestStable = metricsState.latestStableFirmware
|
||||
val latestAlpha = metricsState.latestAlphaFirmware
|
||||
|
||||
val deviceVersion = DeviceVersion(firmwareVersion.substringBeforeLast("."))
|
||||
val statusColor = deviceVersion.determineFirmwareStatusColor(latestStable, latestAlpha)
|
||||
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.installed_firmware_version),
|
||||
icon = Icons.Default.Memory,
|
||||
supportingText = firmwareVersion.substringBeforeLast("."),
|
||||
iconTint = statusColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.latest_stable_firmware),
|
||||
icon = Icons.Default.Memory,
|
||||
supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""),
|
||||
iconTint = MaterialTheme.colorScheme.StatusGreen,
|
||||
onClick = { onFirmwareSelect(latestStable) },
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.latest_alpha_firmware),
|
||||
icon = Icons.Default.Memory,
|
||||
supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""),
|
||||
iconTint = MaterialTheme.colorScheme.StatusYellow,
|
||||
onClick = { onFirmwareSelect(latestAlpha) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceVersion.determineFirmwareStatusColor(
|
||||
latestStable: FirmwareRelease,
|
||||
latestAlpha: FirmwareRelease,
|
||||
): Color {
|
||||
val stableVersion = latestStable.asDeviceVersion()
|
||||
val alphaVersion = latestAlpha.asDeviceVersion()
|
||||
return when {
|
||||
this < stableVersion -> MaterialTheme.colorScheme.StatusRed
|
||||
this == stableVersion -> MaterialTheme.colorScheme.StatusGreen
|
||||
this in stableVersion..alphaVersion -> MaterialTheme.colorScheme.StatusYellow
|
||||
this > alphaVersion -> MaterialTheme.colorScheme.StatusOrange
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.automirrored.outlined.VolumeMute
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.StarBorder
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.QrCode2
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.SettingsItem
|
||||
import org.meshtastic.core.ui.component.SettingsItemSwitch
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
||||
@Composable
|
||||
fun DeviceActions(
|
||||
node: Node,
|
||||
lastTracerouteTime: Long?,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isLocal: Boolean = false,
|
||||
) {
|
||||
var displayFavoriteDialog by remember { mutableStateOf(false) }
|
||||
var displayIgnoreDialog by remember { mutableStateOf(false) }
|
||||
var displayRemoveDialog by remember { mutableStateOf(false) }
|
||||
|
||||
NodeActionDialogs(
|
||||
node = node,
|
||||
displayFavoriteDialog = displayFavoriteDialog,
|
||||
displayIgnoreDialog = displayIgnoreDialog,
|
||||
displayRemoveDialog = displayRemoveDialog,
|
||||
onDismissMenuRequest = {
|
||||
displayFavoriteDialog = false
|
||||
displayIgnoreDialog = false
|
||||
displayRemoveDialog = false
|
||||
},
|
||||
onConfirmFavorite = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Favorite(it))) },
|
||||
onConfirmIgnore = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Ignore(it))) },
|
||||
onConfirmRemove = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(it))) },
|
||||
)
|
||||
TitledCard(title = stringResource(R.string.actions), modifier = modifier) {
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.share_contact),
|
||||
leadingIcon = Icons.Rounded.QrCode2,
|
||||
trailingContent = {},
|
||||
onClick = { onAction(NodeDetailAction.ShareContact) },
|
||||
)
|
||||
if (!isLocal) {
|
||||
RemoteDeviceActions(node = node, lastTracerouteTime = lastTracerouteTime, onAction = onAction)
|
||||
}
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.favorite),
|
||||
leadingIcon = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
|
||||
leadingIconTint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
|
||||
checked = node.isFavorite,
|
||||
onClick = { displayFavoriteDialog = true },
|
||||
)
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.ignore),
|
||||
leadingIcon =
|
||||
if (node.isIgnored) Icons.AutoMirrored.Outlined.VolumeMute else Icons.AutoMirrored.Default.VolumeUp,
|
||||
checked = node.isIgnored,
|
||||
onClick = { displayIgnoreDialog = true },
|
||||
)
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.remove),
|
||||
leadingIcon = Icons.Rounded.Delete,
|
||||
trailingContent = {},
|
||||
onClick = { displayRemoveDialog = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Router
|
||||
import androidx.compose.material.icons.twotone.Verified
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItemDetail
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
|
||||
@Composable
|
||||
fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
|
||||
val node = state.node ?: return
|
||||
val deviceHardware = state.deviceHardware ?: return
|
||||
val hwModelName = deviceHardware.displayName
|
||||
val isSupported = deviceHardware.activelySupported
|
||||
TitledCard(stringResource(R.string.device), modifier = modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.align(Alignment.CenterHorizontally)
|
||||
.size(100.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = Color(node.colors.second).copy(alpha = .5f), shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
DeviceHardwareImage(deviceHardware, Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.hardware),
|
||||
icon = Icons.Default.Router,
|
||||
supportingText = hwModelName,
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text =
|
||||
if (isSupported) {
|
||||
stringResource(R.string.supported)
|
||||
} else {
|
||||
stringResource(R.string.supported_by_community)
|
||||
},
|
||||
icon =
|
||||
if (isSupported) {
|
||||
Icons.TwoTone.Verified
|
||||
} else {
|
||||
ImageVector.vectorResource(org.meshtastic.feature.node.R.drawable.unverified)
|
||||
},
|
||||
supportingText = null,
|
||||
iconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceHardwareImage(deviceHardware: DeviceHardware, modifier: Modifier = Modifier) {
|
||||
val hwImg = deviceHardware.images?.getOrNull(1) ?: deviceHardware.images?.getOrNull(0) ?: "unknown.svg"
|
||||
val imageUrl = "https://flasher.meshtastic.org/img/devices/$hwImg"
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).build(),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
error = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
fallback = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
modifier = modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Air
|
||||
import androidx.compose.material.icons.filled.BlurOn
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Height
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.material.icons.filled.Scale
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.Thermostat
|
||||
import androidx.compose.material.icons.filled.WaterDrop
|
||||
import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
import org.meshtastic.core.model.util.toSmallDistanceString
|
||||
import org.meshtastic.core.model.util.toSpeedString
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.feature.node.model.DrawableMetricInfo
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
internal fun EnvironmentMetrics(
|
||||
node: Node,
|
||||
displayUnits: ConfigProtos.Config.DisplayConfig.DisplayUnits,
|
||||
isFahrenheit: Boolean = false,
|
||||
) {
|
||||
val vectorMetrics =
|
||||
remember(node.environmentMetrics, isFahrenheit, displayUnits) {
|
||||
buildList {
|
||||
with(node.environmentMetrics) {
|
||||
if (hasTemperature()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.temperature,
|
||||
temperature.toTempString(isFahrenheit),
|
||||
Icons.Default.Thermostat,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasRelativeHumidity()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.humidity,
|
||||
"%.0f%%".format(relativeHumidity),
|
||||
Icons.Default.WaterDrop,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasBarometricPressure()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.pressure,
|
||||
"%.0f hPa".format(barometricPressure),
|
||||
Icons.Default.Speed,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasGasResistance()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.gas_resistance,
|
||||
"%.0f MΩ".format(gasResistance),
|
||||
Icons.Default.BlurOn,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasVoltage()) {
|
||||
add(VectorMetricInfo(R.string.voltage, "%.2fV".format(voltage), Icons.Default.Bolt))
|
||||
}
|
||||
if (hasCurrent()) {
|
||||
add(VectorMetricInfo(R.string.current, "%.1fmA".format(current), Icons.Default.Power))
|
||||
}
|
||||
if (hasIaq()) add(VectorMetricInfo(R.string.iaq, iaq.toString(), Icons.Default.Air))
|
||||
if (hasDistance()) {
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.distance,
|
||||
distance.toSmallDistanceString(displayUnits),
|
||||
Icons.Default.Height,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasLux()) add(VectorMetricInfo(R.string.lux, "%.0f lx".format(lux), Icons.Default.LightMode))
|
||||
if (hasUvLux()) {
|
||||
add(VectorMetricInfo(R.string.uv_lux, "%.0f lx".format(uvLux), Icons.Default.LightMode))
|
||||
}
|
||||
if (hasWindSpeed()) {
|
||||
@Suppress("MagicNumber")
|
||||
val normalizedBearing = (windDirection + 180) % 360
|
||||
add(
|
||||
VectorMetricInfo(
|
||||
R.string.wind,
|
||||
windSpeed.toSpeedString(displayUnits),
|
||||
Icons.Outlined.Navigation,
|
||||
normalizedBearing.toFloat(),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasWeight()) {
|
||||
add(VectorMetricInfo(R.string.weight, "%.2f kg".format(weight), Icons.Default.Scale))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val drawableMetrics =
|
||||
remember(node.environmentMetrics, isFahrenheit) {
|
||||
buildList {
|
||||
with(node.environmentMetrics) {
|
||||
if (hasTemperature() && hasRelativeHumidity()) {
|
||||
val dewPoint = UnitConversions.calculateDewPoint(temperature, relativeHumidity)
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
R.string.dew_point,
|
||||
dewPoint.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.ic_outlined_dew_point_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasSoilTemperature()) {
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
R.string.soil_temperature,
|
||||
soilTemperature.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.soil_temperature,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasSoilMoisture()) {
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
R.string.soil_moisture,
|
||||
"%d%%".format(soilMoisture),
|
||||
org.meshtastic.feature.node.R.drawable.soil_moisture,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (hasRadiation()) {
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
R.string.radiation,
|
||||
"%.1f µR/h".format(radiation),
|
||||
org.meshtastic.feature.node.R.drawable.ic_filled_radioactive_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
vectorMetrics.forEach { metric ->
|
||||
InfoCard(
|
||||
icon = metric.icon,
|
||||
text = stringResource(metric.label),
|
||||
value = metric.value,
|
||||
rotateIcon = metric.rotateIcon,
|
||||
)
|
||||
}
|
||||
drawableMetrics.forEach { metric ->
|
||||
DrawableInfoCard(
|
||||
iconRes = metric.icon,
|
||||
text = stringResource(metric.label),
|
||||
value = metric.value,
|
||||
rotateIcon = metric.rotateIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.strings.R
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(rememberScrollState()).padding(16.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(text = firmwareRelease.title, style = MaterialTheme.typography.titleLarge)
|
||||
Text(text = "Version: ${firmwareRelease.id}", style = MaterialTheme.typography.bodyMedium)
|
||||
Markdown(modifier = Modifier.padding(8.dp), content = firmwareRelease.releaseNotes)
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, firmwareRelease.pageUrl.toUri())
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_no_app_to_handle_link, Toast.LENGTH_LONG).show()
|
||||
Timber.e(e)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Link, contentDescription = stringResource(id = R.string.view_release))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(id = R.string.view_release))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, firmwareRelease.zipUrl.toUri())
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.error_no_app_to_handle_link, Toast.LENGTH_LONG).show()
|
||||
Timber.e(e)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(id = R.string.download))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(id = R.string.download))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Card
|
||||
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.draw.rotate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun InfoCard(icon: ImageVector, text: String, value: String, modifier: Modifier = Modifier, rotateIcon: Float = 0f) {
|
||||
Card(modifier = modifier.padding(4.dp).width(100.dp).height(100.dp)) {
|
||||
Box(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp), contentAlignment = Alignment.Center) {
|
||||
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text,
|
||||
modifier = Modifier.size(24.dp).thenIf(rotateIcon != 0f) { rotate(rotateIcon) },
|
||||
)
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DrawableInfoCard(@DrawableRes iconRes: Int, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
Card(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp)) {
|
||||
Box(modifier = Modifier.padding(4.dp).width(100.dp).height(100.dp), contentAlignment = Alignment.Center) {
|
||||
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = text,
|
||||
modifier = Modifier.size(24.dp).thenIf(rotateIcon != 0f) { rotate(rotateIcon) },
|
||||
)
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Modifier.thenIf(precondition: Boolean, action: Modifier.() -> Modifier): Modifier =
|
||||
if (precondition) action() else this
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Navigation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Preview(name = "Wind Dir -359°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionn359() {
|
||||
PreviewWindDirectionItem(-359f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 0°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection0() {
|
||||
PreviewWindDirectionItem(0f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 45°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection45() {
|
||||
PreviewWindDirectionItem(45f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 90°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection90() {
|
||||
PreviewWindDirectionItem(90f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 180°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection180() {
|
||||
PreviewWindDirectionItem(180f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 225°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection225() {
|
||||
PreviewWindDirectionItem(225f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 270°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection270() {
|
||||
PreviewWindDirectionItem(270f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir 315°")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirection315() {
|
||||
PreviewWindDirectionItem(315f)
|
||||
}
|
||||
|
||||
@Preview(name = "Wind Dir -45")
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionN45() {
|
||||
PreviewWindDirectionItem(-45f)
|
||||
}
|
||||
|
||||
@Suppress("detekt:MagicNumber")
|
||||
@Composable
|
||||
private fun PreviewWindDirectionItem(windDirection: Float, windSpeed: String = "5 m/s") {
|
||||
val normalizedBearing = (windDirection + 180) % 360
|
||||
InfoCard(icon = Icons.Outlined.Navigation, text = "Wind", value = windSpeed, rotateIcon = normalizedBearing)
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude
|
|||
val style =
|
||||
SpanStyle(
|
||||
color = HyperlinkBlue,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
fontStyle = MaterialTheme.typography.titleLarge.fontStyle,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
)
|
||||
|
||||
|
|
@ -74,6 +74,7 @@ fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude
|
|||
},
|
||||
),
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItem
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
||||
@Composable
|
||||
@Suppress("MultipleEmitters")
|
||||
fun MetricsSection(
|
||||
node: Node,
|
||||
metricsState: MetricsState,
|
||||
availableLogs: Set<LogsType>,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (node.hasEnvironmentMetrics) {
|
||||
TitledCard(stringResource(R.string.environment), modifier = modifier) {}
|
||||
EnvironmentMetrics(node, isFahrenheit = metricsState.isFahrenheit, displayUnits = metricsState.displayUnits)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (node.hasPowerMetrics) {
|
||||
TitledCard(stringResource(R.string.power), modifier = modifier) {}
|
||||
PowerMetrics(node)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
val nonPositionLogs = availableLogs.filter { it != LogsType.NODE_MAP && it != LogsType.POSITIONS }
|
||||
|
||||
if (nonPositionLogs.isNotEmpty()) {
|
||||
TitledCard(title = stringResource(id = R.string.logs), modifier = modifier) {
|
||||
nonPositionLogs.forEach { type ->
|
||||
SettingsItem(text = stringResource(type.titleRes), leadingIcon = type.icon) {
|
||||
onAction(NodeDetailAction.Navigate(type.route))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.KeyOff
|
||||
import androidx.compose.material.icons.filled.Numbers
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Work
|
||||
import androidx.compose.material.icons.outlined.NoCell
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.twotone.Person
|
||||
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.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.SettingsItemDetail
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
|
||||
@Composable
|
||||
fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) {
|
||||
TitledCard(title = stringResource(R.string.details), modifier = modifier) {
|
||||
if (node.mismatchKey) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyOff,
|
||||
contentDescription = stringResource(id = R.string.encryption_error),
|
||||
tint = Color.Red,
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.encryption_error),
|
||||
style = MaterialTheme.typography.titleLarge.copy(color = Color.Red),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.encryption_error_text),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
MainNodeDetails(node)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainNodeDetails(node: Node) {
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.long_name),
|
||||
icon = Icons.TwoTone.Person,
|
||||
supportingText = node.user.longName.ifEmpty { "???" },
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.short_name),
|
||||
icon = Icons.Outlined.Person,
|
||||
supportingText = node.user.shortName.ifEmpty { "???" },
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.node_number),
|
||||
icon = Icons.Default.Numbers,
|
||||
supportingText = node.num.toUInt().toString(),
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.user_id),
|
||||
icon = Icons.Default.Person,
|
||||
supportingText = node.user.id,
|
||||
)
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.role),
|
||||
icon = Icons.Default.Work,
|
||||
supportingText = node.user.role.name,
|
||||
)
|
||||
if (node.isEffectivelyUnmessageable) {
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.unmonitored_or_infrastructure),
|
||||
icon = Icons.Outlined.NoCell,
|
||||
supportingText = null,
|
||||
)
|
||||
}
|
||||
if (node.deviceMetrics.uptimeSeconds > 0) {
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.uptime),
|
||||
icon = Icons.Default.CheckCircle,
|
||||
supportingText = formatUptime(node.deviceMetrics.uptimeSeconds),
|
||||
)
|
||||
}
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.node_sort_last_heard),
|
||||
icon = Icons.Default.History,
|
||||
supportingText = formatAgo(node.lastHeard),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
|
||||
@Composable
|
||||
fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modifier = Modifier) {
|
||||
if (node.isFavorite) {
|
||||
TitledCard(title = stringResource(R.string.notes), modifier = modifier) {
|
||||
val originalNotes = node.notes
|
||||
var notes by remember(node.notes) { mutableStateOf(node.notes) }
|
||||
val edited = notes.trim() != originalNotes.trim()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
OutlinedTextField(
|
||||
value = notes,
|
||||
onValueChange = { notes = it },
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
placeholder = { Text(stringResource(id = R.string.add_a_note)) },
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSaveNotes(node.num, notes.trim())
|
||||
keyboardController?.hide()
|
||||
},
|
||||
enabled = edited,
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(id = R.string.save))
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onDone = {
|
||||
onSaveNotes(node.num, notes.trim())
|
||||
keyboardController?.hide()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.SocialDistance
|
||||
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.unit.dp
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
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.TitledCard
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
|
||||
/**
|
||||
* Displays node position details, last update time, distance, and related actions like requesting position and
|
||||
* accessing map/position logs.
|
||||
*/
|
||||
@Composable
|
||||
fun PositionSection(
|
||||
node: Node,
|
||||
ourNode: Node?,
|
||||
metricsState: MetricsState,
|
||||
availableLogs: Set<LogsType>,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val distance = ourNode?.distance(node)?.takeIf { it > 0 }?.toDistanceString(metricsState.displayUnits)
|
||||
val hasValidPosition = node.latitude != 0.0 || node.longitude != 0.0
|
||||
TitledCard(title = stringResource(R.string.position), modifier = modifier) {
|
||||
// Current position coordinates (linked)
|
||||
if (hasValidPosition) {
|
||||
InlineMap(node = node, Modifier.fillMaxWidth().height(200.dp))
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.last_position_update),
|
||||
icon = Icons.Default.LocationOn,
|
||||
supportingContent = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(formatAgo(node.position.time), style = MaterialTheme.typography.titleLarge)
|
||||
LinkedCoordinates(
|
||||
latitude = node.latitude,
|
||||
longitude = node.longitude,
|
||||
nodeName = node.user.longName,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Distance (if available)
|
||||
if (distance != null && distance.isNotEmpty()) {
|
||||
SettingsItemDetail(
|
||||
text = stringResource(R.string.node_sort_distance),
|
||||
icon = Icons.Default.SocialDistance,
|
||||
supportingText = distance,
|
||||
)
|
||||
}
|
||||
|
||||
// Exchange position action
|
||||
if (!node.isEffectivelyUnmessageable) {
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.exchange_position),
|
||||
leadingIcon = Icons.Default.LocationOn,
|
||||
trailingContent = {},
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(node))) },
|
||||
)
|
||||
}
|
||||
|
||||
// Node Map log
|
||||
if (availableLogs.contains(LogsType.NODE_MAP)) {
|
||||
SettingsItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
|
||||
onAction(NodeDetailAction.Navigate(LogsType.NODE_MAP.route))
|
||||
}
|
||||
}
|
||||
|
||||
// Positions Log
|
||||
if (availableLogs.contains(LogsType.POSITIONS)) {
|
||||
SettingsItem(text = stringResource(LogsType.POSITIONS.titleRes), leadingIcon = LogsType.POSITIONS.icon) {
|
||||
onAction(NodeDetailAction.Navigate(LogsType.POSITIONS.route))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
|
||||
/**
|
||||
* Displays environmental metrics for a node, including temperature, humidity, pressure, and other sensor data.
|
||||
*
|
||||
* WARNING: All metrics must be added in pairs (e.g., voltage and current for each channel) due to the display logic,
|
||||
* which arranges metrics in columns of two. If an odd number of metrics is provided, the UI may not display as
|
||||
* intended.
|
||||
*/
|
||||
@Composable
|
||||
internal fun PowerMetrics(node: Node) {
|
||||
val metrics =
|
||||
remember(node.powerMetrics) {
|
||||
buildList {
|
||||
with(node.powerMetrics) {
|
||||
if (ch1Voltage != 0f) {
|
||||
add(VectorMetricInfo(R.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Default.Bolt))
|
||||
add(VectorMetricInfo(R.string.channel_1, "%.1fmA".format(ch1Current), Icons.Default.Power))
|
||||
}
|
||||
if (ch2Voltage != 0f) {
|
||||
add(VectorMetricInfo(R.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Default.Bolt))
|
||||
add(VectorMetricInfo(R.string.channel_2, "%.1fmA".format(ch2Current), Icons.Default.Power))
|
||||
}
|
||||
if (ch3Voltage != 0f) {
|
||||
add(VectorMetricInfo(R.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Default.Bolt))
|
||||
add(VectorMetricInfo(R.string.channel_3, "%.1fmA".format(ch3Current), Icons.Default.Power))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
metrics.chunked(2).forEach { rowMetrics ->
|
||||
Column {
|
||||
rowMetrics.forEach { metric ->
|
||||
InfoCard(icon = metric.icon, text = stringResource(metric.label), value = metric.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
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.SettingsItem
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
|
||||
@Composable
|
||||
internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: (NodeDetailAction) -> Unit) {
|
||||
if (!node.isEffectivelyUnmessageable) {
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.direct_message),
|
||||
leadingIcon = Icons.AutoMirrored.TwoTone.Message,
|
||||
trailingContent = {},
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.DirectMessage(node))) },
|
||||
)
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(id = R.string.exchange_userinfo),
|
||||
leadingIcon = Icons.Default.Person,
|
||||
trailingContent = {},
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
|
||||
)
|
||||
TracerouteButton(
|
||||
lastTracerouteTime = lastTracerouteTime,
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.TraceRoute(node))) },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.model
|
||||
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
|
||||
val Node.isEffectivelyUnmessageable: Boolean
|
||||
get() =
|
||||
if (user.hasIsUnmessagable()) {
|
||||
user.isUnmessagable
|
||||
} else {
|
||||
user.role?.isUnmessageableRole() == true
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChargingStation
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Map
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.Power
|
||||
import androidx.compose.material.icons.filled.Route
|
||||
import androidx.compose.material.icons.filled.SignalCellularAlt
|
||||
import androidx.compose.material.icons.filled.Thermostat
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
enum class LogsType(@StringRes val titleRes: Int, val icon: ImageVector, val route: Route) {
|
||||
DEVICE(R.string.device_metrics_log, Icons.Default.ChargingStation, NodeDetailRoutes.DeviceMetrics),
|
||||
NODE_MAP(R.string.node_map, Icons.Default.Map, NodeDetailRoutes.NodeMap),
|
||||
POSITIONS(R.string.position_log, Icons.Default.LocationOn, NodeDetailRoutes.PositionLog),
|
||||
ENVIRONMENT(R.string.env_metrics_log, Icons.Default.Thermostat, NodeDetailRoutes.EnvironmentMetrics),
|
||||
SIGNAL(R.string.sig_metrics_log, Icons.Default.SignalCellularAlt, NodeDetailRoutes.SignalMetrics),
|
||||
POWER(R.string.power_metrics_log, Icons.Default.Power, NodeDetailRoutes.PowerMetrics),
|
||||
TRACEROUTE(R.string.traceroute_log, Icons.Default.Route, NodeDetailRoutes.TracerouteLog),
|
||||
HOST(R.string.host_metrics_log, Icons.Default.Memory, NodeDetailRoutes.HostMetricsLog),
|
||||
PAX(R.string.pax_metrics_log, Icons.Default.People, NodeDetailRoutes.PaxMetrics),
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
internal data class VectorMetricInfo(
|
||||
@StringRes val label: Int,
|
||||
val value: String,
|
||||
val icon: ImageVector,
|
||||
val rotateIcon: Float = 0f,
|
||||
)
|
||||
|
||||
internal data class DrawableMetricInfo(
|
||||
@StringRes val label: Int,
|
||||
val value: String,
|
||||
@DrawableRes val icon: Int,
|
||||
val rotateIcon: Float = 0f,
|
||||
)
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class MetricsState(
|
||||
val isLocal: Boolean = false,
|
||||
val isManaged: Boolean = true,
|
||||
val isFahrenheit: Boolean = false,
|
||||
val displayUnits: ConfigProtos.Config.DisplayConfig.DisplayUnits =
|
||||
ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC,
|
||||
val node: Node? = null,
|
||||
val deviceMetrics: List<TelemetryProtos.Telemetry> = emptyList(),
|
||||
val signalMetrics: List<MeshProtos.MeshPacket> = emptyList(),
|
||||
val powerMetrics: List<TelemetryProtos.Telemetry> = emptyList(),
|
||||
val hostMetrics: List<TelemetryProtos.Telemetry> = emptyList(),
|
||||
val tracerouteRequests: List<MeshLog> = emptyList(),
|
||||
val tracerouteResults: List<MeshLog> = emptyList(),
|
||||
val positionLogs: List<MeshProtos.Position> = emptyList(),
|
||||
val deviceHardware: DeviceHardware? = null,
|
||||
val isLocalDevice: Boolean = false,
|
||||
val firmwareEdition: MeshProtos.FirmwareEdition? = null,
|
||||
val latestStableFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val latestAlphaFirmware: FirmwareRelease = FirmwareRelease(),
|
||||
val paxMetrics: List<MeshLog> = emptyList(),
|
||||
) {
|
||||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||
|
||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||
|
||||
fun hasPowerMetrics() = powerMetrics.isNotEmpty()
|
||||
|
||||
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||
|
||||
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
||||
|
||||
fun hasHostMetrics() = hostMetrics.isNotEmpty()
|
||||
|
||||
fun hasPaxMetrics() = paxMetrics.isNotEmpty()
|
||||
|
||||
fun deviceMetricsFiltered(timeFrame: TimeFrame): List<TelemetryProtos.Telemetry> {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
return deviceMetrics.filter { it.time >= oldestTime }
|
||||
}
|
||||
|
||||
fun signalMetricsFiltered(timeFrame: TimeFrame): List<MeshProtos.MeshPacket> {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
return signalMetrics.filter { it.rxTime >= oldestTime }
|
||||
}
|
||||
|
||||
fun powerMetricsFiltered(timeFrame: TimeFrame): List<TelemetryProtos.Telemetry> {
|
||||
val oldestTime = timeFrame.calculateOldestTime()
|
||||
return powerMetrics.filter { it.time >= oldestTime }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Empty = MetricsState()
|
||||
}
|
||||
}
|
||||
|
||||
/** Supported time frames used to display data. */
|
||||
@Suppress("MagicNumber")
|
||||
enum class TimeFrame(val seconds: Long, @StringRes val strRes: Int) {
|
||||
TWENTY_FOUR_HOURS(TimeUnit.DAYS.toSeconds(1), R.string.twenty_four_hours),
|
||||
FORTY_EIGHT_HOURS(TimeUnit.DAYS.toSeconds(2), R.string.forty_eight_hours),
|
||||
ONE_WEEK(TimeUnit.DAYS.toSeconds(7), R.string.one_week),
|
||||
TWO_WEEKS(TimeUnit.DAYS.toSeconds(14), R.string.two_weeks),
|
||||
FOUR_WEEKS(TimeUnit.DAYS.toSeconds(28), R.string.four_weeks),
|
||||
MAX(0L, R.string.max),
|
||||
;
|
||||
|
||||
fun calculateOldestTime(): Long = if (this == MAX) {
|
||||
MAX.seconds
|
||||
} else {
|
||||
System.currentTimeMillis() / 1000 - this.seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* The time interval to draw the vertical lines representing time on the x-axis.
|
||||
*
|
||||
* @return seconds epoch seconds
|
||||
*/
|
||||
fun lineInterval(): Long = when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(6)
|
||||
|
||||
FORTY_EIGHT_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(12)
|
||||
|
||||
ONE_WEEK.ordinal,
|
||||
TWO_WEEKS.ordinal,
|
||||
-> TimeUnit.DAYS.toSeconds(1)
|
||||
|
||||
else -> TimeUnit.DAYS.toSeconds(7)
|
||||
}
|
||||
|
||||
/** Used to detect a significant time separation between [TelemetryProtos.Telemetry]s. */
|
||||
fun timeThreshold(): Long = when (this.ordinal) {
|
||||
TWENTY_FOUR_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(6)
|
||||
|
||||
FORTY_EIGHT_HOURS.ordinal -> TimeUnit.HOURS.toSeconds(12)
|
||||
|
||||
else -> TimeUnit.DAYS.toSeconds(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the needed [androidx.compose.ui.unit.Dp] depending on the amount of time being plotted.
|
||||
*
|
||||
* @param time in seconds
|
||||
*/
|
||||
fun dp(screenWidth: Int, time: Long): Dp {
|
||||
val timePerScreen = this.lineInterval()
|
||||
val multiplier = time / timePerScreen
|
||||
val dp = (screenWidth * multiplier).toInt().dp
|
||||
return dp.takeIf { it != 0.dp } ?: screenWidth.dp
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 org.meshtastic.feature.node.model
|
||||
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
|
||||
sealed interface NodeDetailAction {
|
||||
data class Navigate(val route: Route) : NodeDetailAction
|
||||
|
||||
data class TriggerServiceAction(val action: ServiceAction) : NodeDetailAction
|
||||
|
||||
data class HandleNodeMenuAction(val action: NodeMenuAction) : NodeDetailAction
|
||||
|
||||
data object ShareContact : NodeDetailAction
|
||||
}
|
||||
121
feature/node/src/main/res/drawable/hw_unknown.xml
Normal file
121
feature/node/src/main/res/drawable/hw_unknown.xml
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="909.88dp"
|
||||
android:height="546.86dp"
|
||||
android:viewportWidth="909.88"
|
||||
android:viewportHeight="546.86">
|
||||
<path
|
||||
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
|
||||
android:fillColor="#cbcccb"/>
|
||||
<path
|
||||
android:pathData="m34.47,104.75v113.48a3.67,3.67 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75ZM845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17,17 0,0 0,-7.2 -13.92v-70.29z"
|
||||
android:fillColor="#cbcccb"/>
|
||||
<path
|
||||
android:pathData="m99.33,129.75v70.29a17,17 0,0 0,-9.86 -3.14h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5z"
|
||||
android:fillColor="#b7b7b7"/>
|
||||
<path
|
||||
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
|
||||
android:fillColor="#b1a368"/>
|
||||
<path
|
||||
android:pathData="m436.27,3.17h60.88a6.2,4.85 0,0 1,6.2 4.85v77.33h-73.28v-77.33a6.2,4.85 0,0 1,6.2 -4.85z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M425.88,20.14L506.28,20.14A5.55,5.55 0,0 1,511.83 25.69L511.83,70.55A5.55,5.55 0,0 1,506.28 76.1L425.88,76.1A5.55,5.55 0,0 1,420.33 70.55L420.33,25.69A5.55,5.55 0,0 1,425.88 20.14z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m511.8,24.48v47.25a5.52,4.31 0,0 1,-5.55 4.34h-80.37a5.55,4.34 0,0 1,-5.59 -4.34v-47.25a5.55,4.34 0,0 1,5.59 -4.34h80.51a5.52,4.31 0,0 1,5.41 4.34z"
|
||||
android:strokeWidth="3.16706"
|
||||
android:fillColor="#9f9f9e"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
|
||||
android:fillColor="#b1a368"/>
|
||||
<path
|
||||
android:pathData="M845.99,129.75L845.99,520.86L106.53,520.86L106.53,213.96a17.06,17.06 0,0 0,-17.06 -17.06h-27.5a2.5,2.5 0,0 1,-2.5 -2.5v-62.15a2.5,2.5 0,0 1,2.5 -2.5h784m25,-25L34.47,104.75v113.48a3.68,3.68 0,0 0,3.67 3.67h41a2.35,2.35 0,0 1,2.35 2.35L81.49,545.86L870.95,545.86L870.95,104.75Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M99.34,200.04L99.34,129.75"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M25.45,253.39h13.53v148.4h-13.53z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m898.52,135.44h4.69a5.67,5.67 0,0 1,5.67 5.67v84.65a5.67,5.67 0,0 1,-5.67 5.67h-4.69"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m430.64,95.71h71.71a2.55,2.55 0,0 1,2.55 2.55v6.48h-76.8v-6.48a2.55,2.55 0,0 1,2.54 -2.55z"
|
||||
android:strokeWidth="2.04"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M433.29,85.68h65.99v10.03h-65.99z"
|
||||
android:strokeWidth="1.99"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m78.62,152.33a14,14 0,1 0,14 14,13.95 13.95,0 0,0 -14,-14zM78.62,173.83a7.55,7.55 0,1 1,7.54 -7.55,7.55 7.55,0 0,1 -7.54,7.55z"
|
||||
android:fillColor="#9f9f9e"/>
|
||||
<path
|
||||
android:pathData="M78.62,166.28m-7.55,0a7.55,7.55 0,1 1,15.1 0a7.55,7.55 0,1 1,-15.1 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="M78.62,166.28m-13.95,0a13.95,13.95 0,1 1,27.9 0a13.95,13.95 0,1 1,-27.9 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m445.36,440.05c0,11.52 10.38,20.86 23.19,20.86 12.81,0 23.19,-9.34 23.19,-20.86 0,-11.52 -10.38,-20.86 -23.19,-20.86 -12.81,0 -23.19,9.34 -23.19,20.86z"
|
||||
android:strokeWidth="0.458227"
|
||||
android:fillColor="#4d4d4d"/>
|
||||
<path
|
||||
android:pathData="m469.4,538.4c-119.83,0 -217.32,-93.41 -217.32,-208.23 0,-114.82 97.48,-208.23 217.32,-208.23 119.83,0 217.32,93.41 217.32,208.23 0,114.82 -97.48,208.23 -217.32,208.23zM469.4,151.82c-102.64,0 -186.13,80.01 -186.13,178.35 0,98.33 83.5,178.35 186.13,178.35 102.62,0 186.13,-80.02 186.13,-178.35 0,-98.34 -83.51,-178.35 -186.13,-178.35z"
|
||||
android:strokeWidth="0.474832"
|
||||
android:fillColor="#4d4d4d"/>
|
||||
<path
|
||||
android:pathData="m468.56,391.97c-8.54,0 -15.46,-6.23 -15.46,-13.91v-23.51c0,-22.75 19.33,-40.13 36.4,-55.47 12.51,-11.26 25.45,-22.89 25.45,-32.16 0,-23.18 -20.81,-42.04 -46.39,-42.04 -26.01,0 -46.39,18.05 -46.39,41.09 0,7.68 -6.93,13.91 -15.46,13.91 -8.54,0 -15.46,-6.23 -15.46,-13.91 0,-37.99 34.68,-68.9 77.31,-68.9 42.63,0 77.31,31.33 77.31,69.85 0,20.82 -17.55,36.59 -34.51,51.84 -13.45,12.07 -27.34,24.56 -27.34,35.78v23.51c0,7.68 -6.93,13.92 -15.46,13.92z"
|
||||
android:strokeWidth="0.458227"
|
||||
android:fillColor="#4d4d4d"
|
||||
android:strokeColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M12.7,104.75L886.82,104.75A11.7,11.7 0,0 1,898.52 116.45L898.52,534.16A11.7,11.7 0,0 1,886.82 545.86L12.7,545.86A11.7,11.7 0,0 1,1 534.16L1,116.45A11.7,11.7 0,0 1,12.7 104.75z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#050606"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
<path
|
||||
android:pathData="m107.42,363.03 l-0.24,-156.31 -2.99,-3.72 -2.99,-3.72v-34.09,-34.09l150.65,0.05 150.65,0.05 -8.28,3.07c-19.32,7.16 -34.46,14.82 -50.22,25.41 -50.58,33.98 -84.36,88.87 -91.06,147.96 -1.43,12.63 -0.64,44.7 1.39,55.76 7.76,42.44 25.98,77.93 55.68,108.42 17.38,17.85 33.99,30.3 55.43,41.55l11.31,5.93 -134.54,0.02 -134.54,0.02z"
|
||||
android:strokeWidth="0.92"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,11a1,1 0,0 1,1 1a10,10 0,0 1,-5 8.656a1,1 0,0 1,-1.302 -0.268l-0.064,-0.098l-3,-5.19a0.995,0.995 0,0 1,-0.133 -0.542l0.01,-0.11l0.023,-0.106l0.034,-0.106l0.046,-0.1l0.056,-0.094l0.067,-0.089a0.994,0.994 0,0 1,0.165 -0.155l0.098,-0.064a2,2 0,0 0,0.993 -1.57l0.007,-0.163a1,1 0,0 1,0.883 -0.994l0.117,-0.007h6z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,3.344a10,10 0,0 1,10 0a1,1 0,0 1,0.418 1.262l-0.052,0.104l-3,5.19l-0.064,0.098a0.994,0.994 0,0 1,-0.155 0.165l-0.089,0.067a1,1 0,0 1,-0.195 0.102l-0.105,0.034l-0.107,0.022a1.003,1.003 0,0 1,-0.547 -0.07l-0.104,-0.052a2,2 0,0 0,-1.842 -0.082l-0.158,0.082a1,1 0,0 1,-1.302 -0.268l-0.064,-0.098l-3,-5.19a1,1 0,0 1,0.366 -1.366z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,11a1,1 0,0 1,0.993 0.884l0.007,0.117a2,2 0,0 0,0.861 1.645l0.237,0.152a0.994,0.994 0,0 1,0.165 0.155l0.067,0.089l0.056,0.095l0.045,0.099c0.014,0.036 0.026,0.07 0.035,0.106l0.022,0.107l0.011,0.11a0.994,0.994 0,0 1,-0.08 0.437l-0.053,0.104l-3,5.19a1,1 0,0 1,-1.366 0.366a10,10 0,0 1,-5 -8.656a1,1 0,0 1,0.883 -0.993l0.117,-0.007h6z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="M620,440q-25,0 -42.5,-17.5T560,380q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q680,363 680,380q0,25 -17.5,42.5T620,440ZM780,320q-25,0 -42.5,-17.5T720,260q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q840,243 840,260q0,25 -17.5,42.5T780,320ZM780,560q-25,0 -42.5,-17.5T720,500q0,-17 9.5,-34.5t20.5,-32q11,-14.5 20.5,-24l9.5,-9.5 9.5,9.5q9.5,9.5 20.5,24t20.5,32Q840,483 840,500q0,25 -17.5,42.5T780,560ZM360,840q-83,0 -141.5,-58.5T160,640q0,-48 21,-89.5t59,-70.5v-240q0,-50 35,-85t85,-35q50,0 85,35t35,85v240q38,29 59,70.5t21,89.5q0,83 -58.5,141.5T360,840ZM240,640h240q0,-29 -12.5,-54T432,544l-32,-24v-280q0,-17 -11.5,-28.5T360,200q-17,0 -28.5,11.5T320,240v280l-32,24q-23,17 -35.5,42T240,640Z"
|
||||
android:fillColor="#e8eaed"/>
|
||||
</vector>
|
||||
11
feature/node/src/main/res/drawable/soil_moisture.xml
Normal file
11
feature/node/src/main/res/drawable/soil_moisture.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M24.5,30a5.202,5.202 0,0 1,-4.626 -8.08L23.49,16.538a1.217,1.217 0,0 1,2.02 0L29.06,21.815A5.492,5.492 0,0 1,30 24.751,5.385 5.385,0 0,1 24.5,30ZM24.5,18.62 L21.564,22.987A3.208,3.208 0,0 0,24.5 28,3.385 3.385,0 0,0 28,24.751a3.435,3.435 0,0 0,-0.63 -1.867Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
|
||||
|
||||
</vector>
|
||||
11
feature/node/src/main/res/drawable/soil_temperature.xml
Normal file
11
feature/node/src/main/res/drawable/soil_temperature.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
|
||||
|
||||
<path android:fillColor="#000000" android:pathData="M25,30a4.986,4.986 0,0 1,-3 -8.98L22,15a3,3 0,0 1,6 0v6.02A4.986,4.986 0,0 1,25 30ZM25,14a1.001,1.001 0,0 0,-1 1v7.13l-0.497,0.289A2.968,2.968 0,0 0,22 25a3,3 0,0 0,6 0,2.968 2.968,0 0,0 -1.503,-2.581L26,22.13L26,15A1.001,1.001 0,0 0,25 14Z"/>
|
||||
|
||||
</vector>
|
||||
9
feature/node/src/main/res/drawable/unverified.xml
Normal file
9
feature/node/src/main/res/drawable/unverified.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M23,11.99l-2.44,-2.79l0.34,-3.69l-3.61,-0.82L15.4,1.5L12,2.96L8.6,1.5L6.71,4.69L3.1,5.5L3.44,9.2L1,11.99l2.44,2.79l-0.34,3.7l3.61,0.82L8.6,22.5l3.4,-1.47l3.4,1.46l1.89,-3.19l3.61,-0.82l-0.34,-3.69L23,11.99zM19.05,13.47l-0.56,0.65l0.08,0.85l0.18,1.95l-1.9,0.43l-0.84,0.19l-0.44,0.74l-0.99,1.68l-1.78,-0.77L12,18.85l-0.79,0.34l-1.78,0.77l-0.99,-1.67l-0.44,-0.74l-0.84,-0.19l-1.9,-0.43l0.18,-1.96l0.08,-0.85l-0.56,-0.65l-1.29,-1.47l1.29,-1.48l0.56,-0.65L5.43,9.01L5.25,7.07l1.9,-0.43l0.84,-0.19l0.44,-0.74l0.99,-1.68l1.78,0.77L12,5.14l0.79,-0.34l1.78,-0.77l0.99,1.68l0.44,0.74l0.84,0.19l1.9,0.43l-0.18,1.95l-0.08,0.85l0.56,0.65l1.29,1.47L19.05,13.47z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue