refactor(ui): Icon audit and node list item refactor (#4313)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-25 16:43:23 -06:00 committed by GitHub
parent 5db2c9d69c
commit a28aa4d52e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 2178 additions and 702 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,18 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.PermScanWifi
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.rounded.CellTower
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.PermScanWifi
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
@ -286,49 +285,49 @@ enum class NodeDetailRoute(
DEVICE(
Res.string.device,
NodeDetailRoutes.DeviceMetrics::class,
Icons.Default.Router,
Icons.Rounded.Router,
{ metricsVM, onNavigateUp -> DeviceMetricsScreen(metricsVM, onNavigateUp) },
),
POSITION_LOG(
Res.string.position_log,
NodeDetailRoutes.PositionLog::class,
Icons.Default.LocationOn,
Icons.Rounded.LocationOn,
{ metricsVM, onNavigateUp -> PositionLogScreen(metricsVM, onNavigateUp) },
),
ENVIRONMENT(
Res.string.environment,
NodeDetailRoutes.EnvironmentMetrics::class,
Icons.Default.LightMode,
Icons.Rounded.LightMode,
{ metricsVM, onNavigateUp -> EnvironmentMetricsScreen(metricsVM, onNavigateUp) },
),
SIGNAL(
Res.string.signal,
NodeDetailRoutes.SignalMetrics::class,
Icons.Default.CellTower,
Icons.Rounded.CellTower,
{ metricsVM, onNavigateUp -> SignalMetricsScreen(metricsVM, onNavigateUp) },
),
TRACEROUTE(
Res.string.traceroute,
NodeDetailRoutes.TracerouteLog::class,
Icons.Default.PermScanWifi,
Icons.Rounded.PermScanWifi,
{ metricsVM, onNavigateUp -> TracerouteLogScreen(viewModel = metricsVM, onNavigateUp = onNavigateUp) },
),
POWER(
Res.string.power,
NodeDetailRoutes.PowerMetrics::class,
Icons.Default.Power,
Icons.Rounded.Power,
{ metricsVM, onNavigateUp -> PowerMetricsScreen(metricsVM, onNavigateUp) },
),
HOST(
Res.string.host,
NodeDetailRoutes.HostMetricsLog::class,
Icons.Default.Memory,
Icons.Rounded.Memory,
{ metricsVM, onNavigateUp -> HostMetricsLogScreen(metricsVM, onNavigateUp) },
),
PAX(
Res.string.pax,
NodeDetailRoutes.PaxMetrics::class,
Icons.Default.People,
Icons.Rounded.People,
{ metricsVM, onNavigateUp -> PaxMetricsScreen(metricsVM, onNavigateUp) },
),
}

View file

@ -41,8 +41,6 @@ import androidx.compose.foundation.layout.safeDrawingPadding
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.rounded.Wifi
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api
@ -150,6 +148,7 @@ import org.meshtastic.core.ui.icon.Map
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Nodes
import org.meshtastic.core.ui.icon.Settings
import org.meshtastic.core.ui.icon.Wifi
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.share.SharedContactDialog
import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
@ -162,7 +161,7 @@ enum class TopLevelDestination(val label: StringResource, val icon: ImageVector,
Nodes(Res.string.nodes, MeshtasticIcons.Nodes, NodesRoutes.NodesGraph),
Map(Res.string.map, MeshtasticIcons.Map, MapRoutes.Map()),
Settings(Res.string.bottom_nav_settings, MeshtasticIcons.Settings, SettingsRoutes.SettingsGraph()),
Connections(Res.string.connections, Icons.Rounded.Wifi, ConnectionsRoutes.ConnectionsGraph),
Connections(Res.string.connections, MeshtasticIcons.Wifi, ConnectionsRoutes.ConnectionsGraph),
;
companion object {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.contact
import androidx.compose.foundation.layout.Box
@ -28,13 +27,6 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.twotone.VolumeMute
import androidx.compose.material.icons.automirrored.twotone.VolumeUp
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.rounded.QrCode2
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
@ -100,6 +92,13 @@ import org.meshtastic.core.strings.unmute
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.component.smartScrollToTop
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.HardwareModel
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.SelectAll
import org.meshtastic.core.ui.icon.VolumeMuteTwoTone
import org.meshtastic.core.ui.icon.VolumeUpTwoTone
import org.meshtastic.proto.AppOnlyProtos
import java.util.concurrent.TimeUnit
@ -232,7 +231,7 @@ fun ContactsScreen(
),
onClick = onNavigateToShare,
) {
Icon(Icons.Rounded.QrCode2, contentDescription = stringResource(Res.string.share_contact))
Icon(MeshtasticIcons.HardwareModel, contentDescription = stringResource(Res.string.share_contact))
}
},
) { paddingValues ->
@ -445,7 +444,7 @@ private fun SelectionToolbar(
title = { Text(text = "$selectedCount") },
navigationIcon = {
IconButton(onClick = onCloseSelection) {
Icon(Icons.Default.Close, contentDescription = stringResource(Res.string.close_selection))
Icon(MeshtasticIcons.Close, contentDescription = stringResource(Res.string.close_selection))
}
},
actions = {
@ -453,9 +452,9 @@ private fun SelectionToolbar(
Icon(
imageVector =
if (isAllMuted) {
Icons.AutoMirrored.TwoTone.VolumeUp
MeshtasticIcons.VolumeUpTwoTone
} else {
Icons.AutoMirrored.TwoTone.VolumeMute
MeshtasticIcons.VolumeMuteTwoTone
},
contentDescription =
if (isAllMuted) {
@ -466,10 +465,10 @@ private fun SelectionToolbar(
)
}
IconButton(onClick = onDeleteSelected) {
Icon(Icons.Default.Delete, contentDescription = stringResource(Res.string.delete_selection))
Icon(MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.delete_selection))
}
IconButton(onClick = onSelectAll) {
Icon(Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select_all))
Icon(MeshtasticIcons.SelectAll, contentDescription = stringResource(Res.string.select_all))
}
},
)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.sharing
import android.Manifest
@ -38,7 +37,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.rounded.ChevronRight
import androidx.compose.material.icons.twotone.Check
import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.ContentCopy
@ -566,7 +565,7 @@ private fun ModemPresetInfo(modemPresetName: String, onClick: () -> Unit) {
}
Spacer(modifier = Modifier.width(16.dp))
Icon(
imageVector = Icons.Default.ChevronRight,
imageVector = Icons.Rounded.ChevronRight,
contentDescription = stringResource(Res.string.navigate_into_label),
modifier = Modifier.padding(end = 16.dp),
)

View file

@ -190,6 +190,7 @@
<string name="elevation_suffix" translatable="false">MSL</string>
<string name="channel_air_util" translatable="false">ChUtil %.1f%% AirUtilTX %.1f%%</string>
<string name="channel">Channel</string>
<string name="channel_name">Channel Name</string>
<string name="qr_code">QR code</string>
<string name="unknown_username">Unknown Username</string>
@ -410,7 +411,7 @@
<string name="encryption_pkc">Public Key Encryption</string>
<string name="encryption_pkc_text">Direct messages are using the new public key infrastructure for encryption.</string>
<string name="encryption_error">Public key mismatch</string>
<string name="encryption_error_text">The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.</string>
<string name="encryption_error_text">The public key does not match the recorded key. You may remove the node and let it exchange keys again, but this may indicate a more security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.</string>
<string name="userinfo">User Info</string>
<string name="meshtastic_new_nodes_notifications">New node notifications</string>
<string name="more_details">More details</string>

View file

@ -0,0 +1,74 @@
/*
* 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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.channel
import org.meshtastic.core.ui.icon.Channel
import org.meshtastic.core.ui.icon.Counter0
import org.meshtastic.core.ui.icon.Counter1
import org.meshtastic.core.ui.icon.Counter2
import org.meshtastic.core.ui.icon.Counter3
import org.meshtastic.core.ui.icon.Counter4
import org.meshtastic.core.ui.icon.Counter5
import org.meshtastic.core.ui.icon.Counter6
import org.meshtastic.core.ui.icon.Counter7
import org.meshtastic.core.ui.icon.Counter8
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
@Composable
@Suppress("MagicNumber")
fun ChannelInfo(
channel: Int,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
val icon =
when (channel) {
0 -> MeshtasticIcons.Counter0
1 -> MeshtasticIcons.Counter1
2 -> MeshtasticIcons.Counter2
3 -> MeshtasticIcons.Counter3
4 -> MeshtasticIcons.Counter4
5 -> MeshtasticIcons.Counter5
6 -> MeshtasticIcons.Counter6
7 -> MeshtasticIcons.Counter7
8 -> MeshtasticIcons.Counter8
else -> MeshtasticIcons.Channel
}
IconInfo(
modifier = modifier,
icon = icon,
contentDescription = stringResource(Res.string.channel),
text = stringResource(Res.string.channel),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun ChannelInfoPreview() {
AppTheme { ChannelInfo(channel = 2) }
}

View file

@ -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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.distance
import org.meshtastic.core.ui.icon.Distance
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun DistanceInfo(
distance: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Distance,
contentDescription = stringResource(Res.string.distance),
text = distance,
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun DistanceInfoPreview() {
AppTheme { DistanceInfo(distance = "423 mi.") }
}

View file

@ -0,0 +1,55 @@
/*
* 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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.altitude
import org.meshtastic.core.strings.elevation_suffix
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
@Composable
fun ElevationInfo(
modifier: Modifier = Modifier,
altitude: Int,
system: DisplayUnits,
suffix: String = stringResource(Res.string.elevation_suffix),
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Elevation,
contentDescription = stringResource(Res.string.altitude),
text = altitude.metersIn(system).toString(system) + " " + suffix,
contentColor = contentColor,
)
}
@Composable
@Preview
private fun ElevationInfoPreview() {
MaterialTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
}

View file

@ -0,0 +1,46 @@
/*
* 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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.hops_away
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Hops,
contentDescription = stringResource(Res.string.hops_away),
text = hops.toString(),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun HopsInfoPreview() {
AppTheme { HopsInfo(hops = 3) }
}

View file

@ -0,0 +1,70 @@
/*
* 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.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
private const val SIZE_ICON = 20
@Composable
fun IconInfo(
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
content: @Composable () -> Unit = {},
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = icon,
contentDescription = contentDescription,
tint = contentColor,
)
text?.let { Text(text = it, style = style, color = contentColor) }
content()
}
}
@Composable
@Preview
private fun IconInfoPreview() {
MaterialTheme {
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", content = { Text(text = "100") })
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.foundation.background
@ -31,9 +30,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@ -61,6 +57,9 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.air_quality_icon
import org.meshtastic.core.strings.close
import org.meshtastic.core.strings.indoor_air_quality_iaq
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.ThumbUp
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.theme.IAQColors.IAQDangerouslyPolluted
import org.meshtastic.core.ui.theme.IAQColors.IAQExcellent
import org.meshtastic.core.ui.theme.IAQColors.IAQExtremelyPolluted
@ -137,7 +136,7 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
Text(text = "IAQ $iaq", color = Color.White, fontWeight = FontWeight.Bold)
Icon(
imageVector =
if (iaqEnum.range.first < 100) Icons.Default.ThumbUp else Icons.Filled.Warning,
if (iaqEnum.range.first < 100) MeshtasticIcons.ThumbUp else MeshtasticIcons.Warning,
contentDescription = stringResource(Res.string.air_quality_icon),
tint = Color.White,
)

View file

@ -0,0 +1,52 @@
/*
* 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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.node_sort_last_heard
import org.meshtastic.core.ui.R
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.formatAgo
@Composable
fun LastHeardInfo(
modifier: Modifier = Modifier,
lastHeard: Int,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
contentDescription = stringResource(Res.string.node_sort_last_heard),
text = formatAgo(lastHeard),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun LastHeardInfoPreview() {
AppTheme { LastHeardInfo(lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600) }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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("MagicNumber")
package org.meshtastic.core.ui.component
@ -29,10 +28,10 @@ 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.filled.SignalCellular4Bar
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.SignalCellularAlt1Bar
import androidx.compose.material.icons.filled.SignalCellularAlt2Bar
import androidx.compose.material.icons.rounded.SignalCellular4Bar
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.SignalCellularAlt1Bar
import androidx.compose.material.icons.rounded.SignalCellularAlt2Bar
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
@ -72,10 +71,10 @@ enum class Quality(
@Stable val imageVector: ImageVector,
@Stable val color: @Composable () -> Color,
) {
NONE(Res.string.none_quality, Icons.Default.SignalCellularAlt1Bar, { colorScheme.StatusRed }),
BAD(Res.string.bad, Icons.Default.SignalCellularAlt2Bar, { colorScheme.StatusOrange }),
FAIR(Res.string.fair, Icons.Default.SignalCellularAlt, { colorScheme.StatusYellow }),
GOOD(Res.string.good, Icons.Default.SignalCellular4Bar, { colorScheme.StatusGreen }),
NONE(Res.string.none_quality, Icons.Rounded.SignalCellularAlt1Bar, { colorScheme.StatusRed }),
BAD(Res.string.bad, Icons.Rounded.SignalCellularAlt2Bar, { colorScheme.StatusOrange }),
FAIR(Res.string.fair, Icons.Rounded.SignalCellularAlt, { colorScheme.StatusYellow }),
GOOD(Res.string.good, Icons.Rounded.SignalCellular4Bar, { colorScheme.StatusGreen }),
}
/**

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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 android.util.Base64
@ -28,10 +27,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -52,7 +47,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
@ -61,7 +55,6 @@ import com.google.protobuf.ByteString
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.R
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.config_security_public_key
import org.meshtastic.core.strings.encryption_error
@ -74,6 +67,10 @@ import org.meshtastic.core.strings.security_icon_help_dismiss
import org.meshtastic.core.strings.security_icon_help_show_all
import org.meshtastic.core.strings.security_icon_help_show_less
import org.meshtastic.core.strings.show_all_key_title
import org.meshtastic.core.ui.icon.KeyOff
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.LockOpen
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@ -106,11 +103,9 @@ fun NodeKeyStatusIcon(
val (icon, tint) =
when {
mismatchKey -> Icons.Default.KeyOff to colorScheme.StatusRed
hasPKC -> Icons.Default.Lock to colorScheme.StatusGreen
else ->
ImageVector.vectorResource(org.meshtastic.core.ui.R.drawable.ic_lock_open_right_24) to
colorScheme.StatusYellow
mismatchKey -> MeshtasticIcons.KeyOff to colorScheme.StatusRed
hasPKC -> MeshtasticIcons.Lock to colorScheme.StatusGreen
else -> MeshtasticIcons.LockOpen to colorScheme.StatusYellow
}
IconButton(onClick = { showEncryptionDialog = true }, modifier = modifier) {
@ -149,7 +144,7 @@ enum class NodeKeySecurityState(
) {
// State for public key mismatch
PKM(
icon = Icons.Default.KeyOff,
icon = MeshtasticIcons.KeyOff,
color = { colorScheme.StatusRed },
descriptionResId = Res.string.encryption_error,
helpTextResId = Res.string.encryption_error_text,
@ -158,7 +153,7 @@ enum class NodeKeySecurityState(
// State for public key encryption
PKC(
icon = Icons.Default.Lock,
icon = MeshtasticIcons.Lock,
color = { colorScheme.StatusGreen },
title = Res.string.encryption_pkc,
helpTextResId = Res.string.encryption_pkc_text,
@ -167,7 +162,7 @@ enum class NodeKeySecurityState(
// State for shared key encryption
PSK(
icon = Icons.Default.LockOpen,
icon = MeshtasticIcons.LockOpen,
color = { colorScheme.StatusYellow },
title = Res.string.encryption_psk,
helpTextResId = Res.string.encryption_psk_text,

View file

@ -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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.sats
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Satellites
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SatelliteCountInfo(
modifier: Modifier = Modifier,
satCount: Int,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Satellites,
contentDescription = stringResource(Res.string.sats),
text = "$satCount",
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun SatelliteCountInfoPreview() {
AppTheme { SatelliteCountInfo(satCount = 5) }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.core.ui.component
@ -30,10 +29,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
@ -78,6 +73,10 @@ import org.meshtastic.core.strings.security_icon_insecure_no_precise
import org.meshtastic.core.strings.security_icon_insecure_precise_only
import org.meshtastic.core.strings.security_icon_secure
import org.meshtastic.core.strings.security_icon_warning_precise_mqtt
import org.meshtastic.core.ui.icon.Lock
import org.meshtastic.core.ui.icon.LockOpen
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@ -109,7 +108,7 @@ enum class SecurityState(
) {
/** State for a secure channel (green lock). */
SECURE(
icon = Icons.Filled.Lock,
icon = MeshtasticIcons.Lock,
color = { colorScheme.StatusGreen },
descriptionResId = Res.string.security_icon_secure,
helpTextResId = Res.string.security_icon_help_green_lock,
@ -120,7 +119,7 @@ enum class SecurityState(
* warning. (yellow open lock)
*/
INSECURE_NO_PRECISE(
icon = Icons.Filled.LockOpen,
icon = MeshtasticIcons.LockOpen,
color = { colorScheme.StatusYellow },
descriptionResId = Res.string.security_icon_insecure_no_precise,
helpTextResId = Res.string.security_icon_help_yellow_open_lock,
@ -131,7 +130,7 @@ enum class SecurityState(
* lock)
*/
INSECURE_PRECISE_ONLY(
icon = Icons.Filled.LockOpen,
icon = MeshtasticIcons.LockOpen,
color = { colorScheme.StatusRed },
descriptionResId = Res.string.security_icon_insecure_precise_only,
helpTextResId = Res.string.security_icon_help_red_open_lock,
@ -142,11 +141,11 @@ enum class SecurityState(
* badge).
*/
INSECURE_PRECISE_MQTT_WARNING(
icon = Icons.Filled.LockOpen,
icon = MeshtasticIcons.LockOpen,
color = { colorScheme.StatusRed },
descriptionResId = Res.string.security_icon_warning_precise_mqtt,
helpTextResId = Res.string.security_icon_help_warning_precise_mqtt,
badgeIcon = Icons.Filled.Warning,
badgeIcon = MeshtasticIcons.Warning,
badgeIconColor = { colorScheme.StatusYellow },
),
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,16 +14,12 @@
* 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.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -34,12 +30,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.channel_air_util
import org.meshtastic.core.strings.hops_away
import org.meshtastic.core.strings.air_utilization
import org.meshtastic.core.strings.channel_utilization
import org.meshtastic.core.strings.signal
import org.meshtastic.core.strings.signal_quality
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.icon.AirUtilization
import org.meshtastic.core.ui.icon.ChannelUtilization
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
const val MAX_VALID_SNR = 100F
@ -53,67 +53,53 @@ fun SignalInfo(
isThisNode: Boolean,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
val text =
if (isThisNode) {
stringResource(Res.string.channel_air_util)
.format(node.deviceMetrics.channelUtilization, node.deviceMetrics.airUtilTx)
} else {
buildList {
val hopsString =
"%s: %s"
.format(
stringResource(Res.string.hops_away),
if (node.hopsAway == -1) {
"?"
} else {
node.hopsAway.toString()
},
)
if (node.channel > 0) {
add("ch:${node.channel}")
}
if (node.hopsAway != 0) add(hopsString)
}
.joinToString(" ")
}
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (text.isNotEmpty()) {
Text(text = text, color = contentColor, style = MaterialTheme.typography.labelSmall)
}
/* We only know the Signal Quality from direct nodes aka 0 hop. */
if (node.hopsAway <= 0) {
if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) {
val quality = determineSignalQuality(node.snr, node.rssi)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Snr(node.snr)
Rssi(node.rssi)
if (isThisNode) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
IconInfo(
icon = MeshtasticIcons.ChannelUtilization,
contentDescription = stringResource(Res.string.channel_utilization),
text = "%.1f%%".format(node.deviceMetrics.channelUtilization),
contentColor = contentColor,
)
IconInfo(
icon = MeshtasticIcons.AirUtilization,
contentDescription = stringResource(Res.string.air_utilization),
text = "%.1f%%".format(node.deviceMetrics.airUtilTx),
contentColor = contentColor,
)
}
} else {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (node.channel > 0) {
ChannelInfo(channel = node.channel, contentColor = contentColor)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
tint = quality.color.invoke(),
)
Text(
text = "${stringResource(Res.string.signal)} ${stringResource(quality.nameRes)}",
style = MaterialTheme.typography.labelSmall,
color = contentColor,
maxLines = 1,
)
if (node.hopsAway > 0) {
HopsInfo(hops = node.hopsAway, contentColor = contentColor)
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
if (node.snr < MAX_VALID_SNR && node.rssi < MAX_VALID_RSSI) {
val quality = determineSignalQuality(node.snr, node.rssi)
Snr(node.snr)
Rssi(node.rssi)
IconInfo(
icon = quality.imageVector,
contentDescription = stringResource(Res.string.signal_quality),
contentColor = quality.color.invoke(),
text = "${stringResource(Res.string.signal)} ${stringResource(quality.nameRes)}",
)
}
}
}
}
}
if (node.deviceMetrics.uptimeSeconds > 0) {
UptimeInfo(uptime = formatUptime(node.deviceMetrics.uptimeSeconds), contentColor = contentColor)
}
}
}

View file

@ -0,0 +1,265 @@
/*
* 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/>.
*/
@file:Suppress("TooManyFunctions")
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.env_metrics_log
import org.meshtastic.core.strings.node_id
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.role
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.ui.icon.AirQuality
import org.meshtastic.core.ui.icon.ArrowCircleUp
import org.meshtastic.core.ui.icon.HardwareModel
import org.meshtastic.core.ui.icon.Humidity
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.NodeId
import org.meshtastic.core.ui.icon.Paxcount
import org.meshtastic.core.ui.icon.Power
import org.meshtastic.core.ui.icon.Pressure
import org.meshtastic.core.ui.icon.Role
import org.meshtastic.core.ui.icon.Soil
import org.meshtastic.core.ui.icon.Temperature
private const val SIZE_ICON = 20
@Composable
fun TemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Temperature,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun HumidityInfo(
humidity: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Humidity,
contentDescription = stringResource(Res.string.env_metrics_log),
text = humidity,
contentColor = contentColor,
)
}
@Composable
fun PressureInfo(
pressure: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Pressure,
contentDescription = stringResource(Res.string.env_metrics_log),
text = pressure,
contentColor = contentColor,
)
}
@Composable
fun SoilTemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
OverlayIconInfo(
modifier = modifier,
icon = MeshtasticIcons.Soil,
overlayIcon = MeshtasticIcons.Temperature,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun SoilMoistureInfo(
moisture: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
OverlayIconInfo(
modifier = modifier,
icon = MeshtasticIcons.Soil,
overlayIcon = MeshtasticIcons.Humidity,
contentDescription = stringResource(Res.string.env_metrics_log),
text = moisture,
contentColor = contentColor,
)
}
@Composable
fun PaxcountInfo(
pax: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Paxcount,
contentDescription = stringResource(Res.string.pax_metrics_log),
text = pax,
contentColor = contentColor,
)
}
@Composable
fun AirQualityInfo(
iaq: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.AirQuality,
contentDescription = stringResource(Res.string.env_metrics_log),
text = iaq,
contentColor = contentColor,
)
}
@Composable
fun PowerInfo(value: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Power,
contentDescription = stringResource(Res.string.env_metrics_log),
text = value,
contentColor = contentColor,
)
}
@Composable
fun UptimeInfo(
uptime: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.ArrowCircleUp,
contentDescription = stringResource(Res.string.uptime),
text = uptime,
contentColor = contentColor,
)
}
@Composable
fun HardwareInfo(
hwModel: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.HardwareModel,
contentDescription = "Hardware Model",
text = hwModel,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun RoleInfo(role: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Role,
contentDescription = stringResource(Res.string.role),
text = role,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun NodeIdInfo(id: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.NodeId,
contentDescription = stringResource(Res.string.node_id),
text = id,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
@Suppress("MagicNumber")
fun OverlayIconInfo(
icon: ImageVector,
overlayIcon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
val foregroundPainter = rememberVectorPainter(overlayIcon)
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = contentColor,
modifier =
Modifier.size(SIZE_ICON.dp).drawWithContent {
drawContent()
val badgeSize = size.width * .5f
with(foregroundPainter) {
draw(size = Size(badgeSize, badgeSize), colorFilter = ColorFilter.tint(contentColor))
}
},
)
text?.let { Text(text = it, style = style, color = contentColor) }
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -120,6 +119,9 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
voltage = 3.7F
current = 0.0F
iaq = 100
barometricPressure = 1013.25F
soilTemperature = 28.0F
soilMoisture = 50
},
paxcounter =
paxcount {

View file

@ -0,0 +1,81 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.CloudDownload
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Folder
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material.icons.rounded.SystemUpdate
import androidx.compose.material.icons.rounded.ThumbUp
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Add: ImageVector
get() = Icons.Rounded.Add
val MeshtasticIcons.AddReaction: ImageVector
get() = Icons.Rounded.AddReaction
val MeshtasticIcons.Clear: ImageVector
get() = Icons.Rounded.Clear
val MeshtasticIcons.Close: ImageVector
get() = Icons.Rounded.Close
val MeshtasticIcons.Copy: ImageVector
get() = Icons.Rounded.ContentCopy
val MeshtasticIcons.Delete: ImageVector
get() = Icons.Rounded.Delete
val MeshtasticIcons.Edit: ImageVector
get() = Icons.Rounded.Edit
val MeshtasticIcons.More: ImageVector
get() = Icons.Rounded.MoreVert
val MeshtasticIcons.Refresh: ImageVector
get() = Icons.Rounded.Refresh
val MeshtasticIcons.Reply: ImageVector
get() = Icons.AutoMirrored.Filled.Reply
val MeshtasticIcons.Save: ImageVector
get() = Icons.Rounded.Save
val MeshtasticIcons.Search: ImageVector
get() = Icons.Rounded.Search
val MeshtasticIcons.Send: ImageVector
get() = Icons.AutoMirrored.Filled.Send
val MeshtasticIcons.Share: ImageVector
get() = Icons.Rounded.Share
val MeshtasticIcons.Sort: ImageVector
get() = Icons.AutoMirrored.Filled.Sort
val MeshtasticIcons.CloudDownload: ImageVector
get() = Icons.Rounded.CloudDownload
val MeshtasticIcons.Folder: ImageVector
get() = Icons.Rounded.Folder
val MeshtasticIcons.SystemUpdate: ImageVector
get() = Icons.Rounded.SystemUpdate
val MeshtasticIcons.SelectAll: ImageVector
get() = Icons.Rounded.SelectAll
val MeshtasticIcons.ThumbUp: ImageVector
get() = Icons.Rounded.ThumbUp

View file

@ -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.icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import org.meshtastic.core.ui.R
/** These are from Material Symbols drawables. */
val MeshtasticIcons.Counter0: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_0_24px)
val MeshtasticIcons.Counter1: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_1_24px)
val MeshtasticIcons.Counter2: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_2_24px)
val MeshtasticIcons.Counter3: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_3_24px)
val MeshtasticIcons.Counter4: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_4_24px)
val MeshtasticIcons.Counter5: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_5_24px)
val MeshtasticIcons.Counter6: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_6_24px)
val MeshtasticIcons.Counter7: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_7_24px)
val MeshtasticIcons.Counter8: ImageVector
@Composable get() = ImageVector.vectorResource(R.drawable.counter_8_24px)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,15 +14,25 @@
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Fingerprint
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.rounded.Work
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val MeshtasticIcons.HardwareModel: ImageVector
get() = Icons.Rounded.Router
val MeshtasticIcons.Role: ImageVector
get() = Icons.Rounded.Work
val MeshtasticIcons.NodeId: ImageVector
get() = Icons.Rounded.Fingerprint
/**
* This is from Material Symbols.
*

View file

@ -0,0 +1,30 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.Usb
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Bluetooth: ImageVector
get() = Icons.Rounded.Bluetooth
val MeshtasticIcons.Usb: ImageVector
get() = Icons.Rounded.Usb
val MeshtasticIcons.Wifi: ImageVector
get() = Icons.Rounded.Wifi

View file

@ -0,0 +1,39 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material.icons.rounded.Group
import androidx.compose.material.icons.rounded.Groups
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.PersonOff
import androidx.compose.material.icons.rounded.PersonSearch
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Person: ImageVector
get() = Icons.Rounded.Person
val MeshtasticIcons.PersonOff: ImageVector
get() = Icons.Rounded.PersonOff
val MeshtasticIcons.Groups: ImageVector
get() = Icons.Rounded.Groups
val MeshtasticIcons.Group: ImageVector
get() = Icons.Rounded.Group
val MeshtasticIcons.AccountCircle: ImageVector
get() = Icons.Rounded.AccountCircle
val MeshtasticIcons.PersonSearch: ImageVector
get() = Icons.Rounded.PersonSearch

View file

@ -0,0 +1,39 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Fingerprint
import androidx.compose.material.icons.rounded.KeyOff
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.LockOpen
import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Lock: ImageVector
get() = Icons.Rounded.Lock
val MeshtasticIcons.LockOpen: ImageVector
get() = Icons.Rounded.LockOpen
val MeshtasticIcons.Warning: ImageVector
get() = Icons.Rounded.Warning
val MeshtasticIcons.KeyOff: ImageVector
get() = Icons.Rounded.KeyOff
val MeshtasticIcons.Verified: ImageVector
get() = Icons.Rounded.Verified
val MeshtasticIcons.Fingerprint: ImageVector
get() = Icons.Rounded.Fingerprint

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,15 +14,31 @@
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CrueltyFree
import androidx.compose.material.icons.rounded.Route
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.SsidChart
import androidx.compose.material.icons.rounded.WifiChannel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val MeshtasticIcons.Hops: ImageVector
get() = Icons.Rounded.CrueltyFree
val MeshtasticIcons.Route: ImageVector
get() = Icons.Rounded.Route
val MeshtasticIcons.Channel: ImageVector
get() = Icons.Rounded.WifiChannel
val MeshtasticIcons.ChannelUtilization: ImageVector
get() = Icons.Rounded.SignalCellularAlt
val MeshtasticIcons.AirUtilization: ImageVector
get() = Icons.Rounded.SsidChart
val MeshtasticIcons.SignalCellular0Bar: ImageVector
get() {
if (signalCellular0Bar != null) {

View file

@ -0,0 +1,82 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.SpeakerNotes
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.automirrored.twotone.VolumeMute
import androidx.compose.material.icons.automirrored.twotone.VolumeUp
import androidx.compose.material.icons.rounded.ArrowCircleUp
import androidx.compose.material.icons.rounded.CheckCircleOutline
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.NoCell
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.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
import androidx.compose.material.icons.twotone.CloudOff
import androidx.compose.material.icons.twotone.CloudSync
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Favorite: ImageVector
get() = Icons.Rounded.Star
val MeshtasticIcons.NotFavorite: ImageVector
get() = Icons.Rounded.StarBorder
val MeshtasticIcons.Muted: ImageVector
get() = Icons.Rounded.SpeakerNotesOff
val MeshtasticIcons.Unmuted: ImageVector
get() = Icons.AutoMirrored.Filled.SpeakerNotes
val MeshtasticIcons.VolumeOff: ImageVector
get() = Icons.AutoMirrored.Filled.VolumeOff
val MeshtasticIcons.VolumeUp: ImageVector
get() = Icons.AutoMirrored.Filled.VolumeUp
val MeshtasticIcons.History: ImageVector
get() = Icons.Rounded.History
val MeshtasticIcons.Cloud: ImageVector
get() = Icons.Rounded.Cloud
val MeshtasticIcons.CloudOff: ImageVector
get() = Icons.Rounded.CloudOff
val MeshtasticIcons.Unmessageable: ImageVector
get() = Icons.Rounded.NoCell
val MeshtasticIcons.CloudDone: ImageVector
get() = Icons.TwoTone.CloudDone
val MeshtasticIcons.CloudSync: ImageVector
get() = Icons.TwoTone.CloudSync
val MeshtasticIcons.CloudOffTwoTone: ImageVector
get() = Icons.TwoTone.CloudOff
val MeshtasticIcons.CloudTwoTone: ImageVector
get() = Icons.TwoTone.Cloud
val MeshtasticIcons.ArrowCircleUp: ImageVector
get() = Icons.Rounded.ArrowCircleUp
val MeshtasticIcons.Dangerous: ImageVector
get() = Icons.Rounded.Dangerous
val MeshtasticIcons.VolumeUpTwoTone: ImageVector
get() = Icons.AutoMirrored.TwoTone.VolumeUp
val MeshtasticIcons.VolumeMuteTwoTone: ImageVector
get() = Icons.AutoMirrored.TwoTone.VolumeMute
val MeshtasticIcons.CheckCircle: ImageVector
get() = Icons.Rounded.CheckCircleOutline

View file

@ -0,0 +1,56 @@
/*
* 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.icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.DataArray
import androidx.compose.material.icons.rounded.ElectricBolt
import androidx.compose.material.icons.rounded.Grass
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.SocialDistance
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.StackedLineChart
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.material.icons.twotone.SatelliteAlt
import androidx.compose.ui.graphics.vector.ImageVector
val MeshtasticIcons.Temperature: ImageVector
get() = Icons.Rounded.Thermostat
val MeshtasticIcons.Humidity: ImageVector
get() = Icons.Rounded.WaterDrop
val MeshtasticIcons.Pressure: ImageVector
get() = Icons.Rounded.Speed
val MeshtasticIcons.Soil: ImageVector
get() = Icons.Rounded.Grass
val MeshtasticIcons.Paxcount: ImageVector
get() = Icons.Rounded.People
val MeshtasticIcons.AirQuality: ImageVector
get() = Icons.Rounded.Air
val MeshtasticIcons.Power: ImageVector
get() = Icons.Rounded.ElectricBolt
val MeshtasticIcons.Distance: ImageVector
get() = Icons.Rounded.SocialDistance
val MeshtasticIcons.Satellites: ImageVector
get() = Icons.TwoTone.SatelliteAlt
val MeshtasticIcons.DataArray: ImageVector
get() = Icons.Rounded.DataArray
val MeshtasticIcons.Speed: ImageVector
get() = Icons.Rounded.Speed
val MeshtasticIcons.Chart: ImageVector
get() = Icons.Rounded.StackedLineChart

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM440,680L520,680Q553,680 576.5,656.5Q600,633 600,600L600,360Q600,327 576.5,303.5Q553,280 520,280L440,280Q407,280 383.5,303.5Q360,327 360,360L360,600Q360,633 383.5,656.5Q407,680 440,680ZM440,360L520,360Q520,360 520,360Q520,360 520,360L520,600Q520,600 520,600Q520,600 520,600L440,600Q440,600 440,600Q440,600 440,600L440,360Q440,360 440,360Q440,360 440,360Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM460,360L460,640Q460,657 471.5,668.5Q483,680 500,680Q517,680 528.5,668.5Q540,657 540,640L540,320Q540,303 528.5,291.5Q517,280 500,280L420,280Q403,280 391.5,291.5Q380,303 380,320Q380,337 391.5,348.5Q403,360 420,360L460,360Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM560,680Q577,680 588.5,668.5Q600,657 600,640Q600,623 588.5,611.5Q577,600 560,600L440,600L440,520Q440,520 440,520Q440,520 440,520L520,520Q553,520 576.5,496.5Q600,473 600,440L600,360Q600,327 576.5,303.5Q553,280 520,280L400,280Q383,280 371.5,291.5Q360,303 360,320Q360,337 371.5,348.5Q383,360 400,360L520,360Q520,360 520,360Q520,360 520,360L520,440Q520,440 520,440Q520,440 520,440L440,440Q407,440 383.5,463.5Q360,487 360,520L360,640Q360,657 371.5,668.5Q383,680 400,680L560,680Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM400,680L520,680Q553,680 576.5,656.5Q600,633 600,600L600,540Q600,514 583,497Q566,480 540,480Q566,480 583,463Q600,446 600,420L600,360Q600,327 576.5,303.5Q553,280 520,280L400,280Q383,280 371.5,291.5Q360,303 360,320Q360,337 371.5,348.5Q383,360 400,360L520,360Q520,360 520,360Q520,360 520,360L520,440Q520,440 520,440Q520,440 520,440L480,440Q463,440 451.5,451.5Q440,463 440,480Q440,497 451.5,508.5Q463,520 480,520L520,520Q520,520 520,520Q520,520 520,520L520,600Q520,600 520,600Q520,600 520,600L400,600Q383,600 371.5,611.5Q360,623 360,640Q360,657 371.5,668.5Q383,680 400,680Z"/>
</vector>

View file

@ -0,0 +1,22 @@
<!--
~ Copyright (c) 2025 Meshtastic LLC
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM520,680L600,680L600,280L520,280L520,440L440,440L440,280L360,280L360,520L520,520L520,680Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM400,680L520,680Q553,680 576.5,656.5Q600,633 600,600L600,520Q600,487 576.5,463.5Q553,440 520,440L440,440L440,360L560,360Q577,360 588.5,348.5Q600,337 600,320Q600,303 588.5,291.5Q577,280 560,280L400,280Q383,280 371.5,291.5Q360,303 360,320L360,480Q360,497 371.5,508.5Q383,520 400,520L520,520Q520,520 520,520Q520,520 520,520L520,600Q520,600 520,600Q520,600 520,600L400,600Q383,600 371.5,611.5Q360,623 360,640Q360,657 371.5,668.5Q383,680 400,680Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM440,680L520,680Q553,680 576.5,656.5Q600,633 600,600L600,520Q600,487 576.5,463.5Q553,440 520,440L440,440L440,360Q440,360 440,360Q440,360 440,360L520,360Q537,360 548.5,348.5Q560,337 560,320Q560,303 548.5,291.5Q537,280 520,280L440,280Q407,280 383.5,303.5Q360,327 360,360L360,600Q360,633 383.5,656.5Q407,680 440,680ZM440,520L520,520Q520,520 520,520Q520,520 520,520L520,600Q520,600 520,600Q520,600 520,600L440,600Q440,600 440,600Q440,600 440,600L440,520Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M520,360L452,632Q447,650 459,665Q471,680 490,680Q504,680 514,672Q524,664 527,651L598,370Q600,365 600,361Q600,357 600,352Q600,323 579.5,301.5Q559,280 530,280L400,280Q383,280 371.5,291.5Q360,303 360,320Q360,337 371.5,348.5Q383,360 400,360L520,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM440,680L520,680Q553,680 576.5,656.5Q600,633 600,600L600,540Q600,515 582.5,497.5Q565,480 540,480Q565,480 582.5,462.5Q600,445 600,420L600,360Q600,327 576.5,303.5Q553,280 520,280L440,280Q407,280 383.5,303.5Q360,327 360,360L360,420Q360,445 377.5,462.5Q395,480 420,480Q395,480 377.5,497.5Q360,515 360,540L360,600Q360,633 383.5,656.5Q407,680 440,680ZM440,360L520,360Q520,360 520,360Q520,360 520,360L520,440Q520,440 520,440Q520,440 520,440L440,440Q440,440 440,440Q440,440 440,440L440,360Q440,360 440,360Q440,360 440,360ZM440,600Q440,600 440,600Q440,600 440,600L440,520Q440,520 440,520Q440,520 440,520L520,520Q520,520 520,520Q520,520 520,520L520,600Q520,600 520,600Q520,600 520,600L440,600Z"/>
</vector>

View file

@ -42,16 +42,6 @@ import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Dangerous
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SystemUpdate
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.Usb
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@ -146,6 +136,17 @@ import org.meshtastic.core.strings.i_know_what_i_m_doing
import org.meshtastic.core.strings.learn_more
import org.meshtastic.core.strings.okay
import org.meshtastic.core.strings.save
import org.meshtastic.core.ui.icon.Bluetooth
import org.meshtastic.core.ui.icon.CheckCircle
import org.meshtastic.core.ui.icon.CloudDownload
import org.meshtastic.core.ui.icon.Dangerous
import org.meshtastic.core.ui.icon.Folder
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.SystemUpdate
import org.meshtastic.core.ui.icon.Usb
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.icon.Wifi
private const val CYCLE_DELAY_MS = 4500L
@ -413,7 +414,7 @@ private fun ReadyState(
},
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Icon(Icons.Default.Folder, contentDescription = null)
Icon(MeshtasticIcons.Folder, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.firmware_update_select_file))
}
@ -428,10 +429,10 @@ private fun ReadyState(
Icon(
imageVector =
when (state.updateMethod) {
FirmwareUpdateMethod.Ble -> Icons.Rounded.Bluetooth
FirmwareUpdateMethod.Usb -> Icons.Rounded.Usb
FirmwareUpdateMethod.Wifi -> Icons.Rounded.Wifi
else -> Icons.Default.SystemUpdate
FirmwareUpdateMethod.Ble -> MeshtasticIcons.Bluetooth
FirmwareUpdateMethod.Usb -> MeshtasticIcons.Usb
FirmwareUpdateMethod.Wifi -> MeshtasticIcons.Wifi
else -> MeshtasticIcons.SystemUpdate
},
contentDescription = null,
)
@ -459,7 +460,7 @@ private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismissReques
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Warning,
MeshtasticIcons.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp),
@ -616,7 +617,7 @@ private fun BootloaderWarningCard(deviceHardware: DeviceHardware, onDismissForDe
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Warning,
imageVector = MeshtasticIcons.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
)
@ -708,7 +709,7 @@ private fun ProgressContent(
) {
if (isDownloading) {
Icon(
Icons.Default.CloudDownload,
MeshtasticIcons.CloudDownload,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary,
@ -818,7 +819,7 @@ private fun CyclingMessages() {
@Composable
private fun VerificationFailedState(onRetry: () -> Unit, onIgnore: () -> Unit) {
Icon(
Icons.Default.Warning,
MeshtasticIcons.Warning,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error,
@ -833,7 +834,7 @@ private fun VerificationFailedState(onRetry: () -> Unit, onIgnore: () -> Unit) {
Spacer(Modifier.height(32.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
OutlinedButton(onClick = onRetry) {
Icon(Icons.Default.Refresh, contentDescription = null)
Icon(MeshtasticIcons.Refresh, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.firmware_update_retry))
}
@ -844,7 +845,7 @@ private fun VerificationFailedState(onRetry: () -> Unit, onIgnore: () -> Unit) {
@Composable
private fun ErrorState(error: String, onRetry: () -> Unit) {
Icon(
Icons.Default.Dangerous,
MeshtasticIcons.Dangerous,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error,
@ -858,7 +859,7 @@ private fun ErrorState(error: String, onRetry: () -> Unit) {
)
Spacer(Modifier.height(32.dp))
OutlinedButton(onClick = onRetry) {
Icon(Icons.Default.Refresh, contentDescription = null)
Icon(MeshtasticIcons.Refresh, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text(stringResource(Res.string.firmware_update_retry))
}
@ -871,7 +872,7 @@ private fun SuccessState(onDone: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.CheckCircle,
MeshtasticIcons.CheckCircle,
contentDescription = null,
modifier = Modifier.size(100.dp),
tint = MaterialTheme.colorScheme.primary,

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.feature.map
import android.Manifest // Added for Accompanist
import android.Manifest
import android.graphics.Paint
import android.text.format.DateUtils
import androidx.appcompat.content.res.AppCompatResources
@ -34,14 +34,14 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lens
import androidx.compose.material.icons.filled.LocationDisabled
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Lens
import androidx.compose.material.icons.rounded.LocationDisabled
import androidx.compose.material.icons.rounded.PinDrop
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox
@ -77,8 +77,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kermit.Logger
import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist
import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
@ -758,7 +758,7 @@ fun MapView(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Star,
imageVector = Icons.Rounded.Star,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
@ -783,7 +783,7 @@ fun MapView(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.PinDrop,
imageVector = Icons.Rounded.PinDrop,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
@ -808,7 +808,7 @@ fun MapView(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Lens,
imageVector = Icons.Rounded.Lens,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onSurface,
@ -834,7 +834,7 @@ fun MapView(
if (myLocationOverlay == null) {
Icons.Outlined.MyLocation
} else {
Icons.Default.LocationDisabled
Icons.Rounded.LocationDisabled
},
contentDescription = stringResource(Res.string.toggle_my_position),
) {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.map.component
import androidx.compose.animation.AnimatedVisibility
@ -23,7 +22,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -51,7 +50,7 @@ fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
) {
FloatingActionButton(onClick = onClick, contentColor = MaterialTheme.colorScheme.primary) {
Icon(
imageVector = Icons.Default.Download,
imageVector = Icons.Rounded.Download,
contentDescription = stringResource(Res.string.map_download_region),
modifier = Modifier.scale(1.25f),
)

View file

@ -17,8 +17,6 @@
package org.meshtastic.feature.map.component
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.text.format.DateFormat
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.compose.foundation.Image
@ -37,8 +35,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.rounded.CalendarMonth
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.IconButton
@ -179,7 +177,7 @@ fun EditWaypointDialog(
modifier = Modifier.fillMaxWidth().size(48.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(imageVector = Icons.Default.Lock, contentDescription = stringResource(Res.string.locked))
Image(imageVector = Icons.Rounded.Lock, contentDescription = stringResource(Res.string.locked))
Text(stringResource(Res.string.locked))
Switch(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
@ -221,7 +219,7 @@ fun EditWaypointDialog(
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = Icons.Default.CalendarMonth,
imageVector = Icons.Rounded.CalendarMonth,
contentDescription = stringResource(Res.string.expires),
)
Text(stringResource(Res.string.expires))

View file

@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.filled.TripOrigin
import androidx.compose.material.icons.rounded.TripOrigin
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@ -501,7 +501,7 @@ fun MapView(
},
) {
Icon(
imageVector = androidx.compose.material.icons.Icons.Default.TripOrigin,
imageVector = androidx.compose.material.icons.Icons.Rounded.TripOrigin,
contentDescription = stringResource(Res.string.track_point),
tint = color,
)

View file

@ -34,8 +34,8 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.rounded.CalendarMonth
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
@ -180,7 +180,7 @@ fun EditWaypointDialog(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
imageVector = Icons.Default.Lock,
imageVector = Icons.Rounded.Lock,
contentDescription = stringResource(Res.string.locked),
)
Spacer(modifier = Modifier.width(8.dp))
@ -199,7 +199,7 @@ fun EditWaypointDialog(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
imageVector = Icons.Default.CalendarMonth,
imageVector = Icons.Rounded.CalendarMonth,
contentDescription = stringResource(Res.string.expires),
)
Spacer(modifier = Modifier.width(8.dp))

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,18 +14,17 @@
* 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.map.component
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationDisabled
import androidx.compose.material.icons.filled.Navigation
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.rounded.LocationDisabled
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.MaterialTheme
@ -122,7 +121,7 @@ fun MapControlsOverlay(
MapButton(
icon =
if (isLocationTrackingEnabled) {
Icons.Default.LocationDisabled
Icons.Rounded.LocationDisabled
} else {
Icons.Outlined.MyLocation
},

View file

@ -48,19 +48,19 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ChatBubbleOutline
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.SpeakerNotes
import androidx.compose.material.icons.filled.SpeakerNotesOff
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.rounded.ArrowDownward
import androidx.compose.material.icons.rounded.ChatBubbleOutline
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.FilterList
import androidx.compose.material.icons.rounded.FilterListOff
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material.icons.rounded.SpeakerNotes
import androidx.compose.material.icons.rounded.SpeakerNotesOff
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
@ -499,7 +499,7 @@ private fun BoxScope.ScrollToBottomFab(coroutineScope: CoroutineScope, listState
},
) {
Icon(
imageVector = Icons.Default.ArrowDownward,
imageVector = Icons.Rounded.ArrowDownward,
contentDescription = stringResource(Res.string.scroll_to_bottom),
)
}
@ -683,13 +683,13 @@ private fun ActionModeTopBar(selectedCount: Int, onAction: (MessageMenuAction) -
},
actions = {
IconButton(onClick = { onAction(MessageMenuAction.ClipboardCopy) }) {
Icon(imageVector = Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy))
Icon(imageVector = Icons.Rounded.ContentCopy, contentDescription = stringResource(Res.string.copy))
}
IconButton(onClick = { onAction(MessageMenuAction.Delete) }) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.delete))
Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.delete))
}
IconButton(onClick = { onAction(MessageMenuAction.SelectAll) }) {
Icon(imageVector = Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select_all))
Icon(imageVector = Icons.Rounded.SelectAll, contentDescription = stringResource(Res.string.select_all))
}
},
)
@ -775,7 +775,7 @@ private fun MessageTopBarActions(
var expanded by remember { mutableStateOf(false) }
Box {
IconButton(onClick = { expanded = true }, enabled = true) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(Res.string.overflow_menu))
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = stringResource(Res.string.overflow_menu))
}
OverFlowMenu(
expanded = expanded,
@ -828,7 +828,7 @@ private fun QuickChatToggleMenuItem(showQuickChat: Boolean, onDismiss: () -> Uni
},
leadingIcon = {
Icon(
imageVector = if (showQuickChat) Icons.Default.SpeakerNotesOff else Icons.Default.SpeakerNotes,
imageVector = if (showQuickChat) Icons.Rounded.SpeakerNotesOff else Icons.Rounded.SpeakerNotes,
contentDescription = title,
)
},
@ -844,7 +844,7 @@ private fun QuickChatOptionsMenuItem(onDismiss: () -> Unit, onNavigate: () -> Un
onDismiss()
onNavigate()
},
leadingIcon = { Icon(imageVector = Icons.Default.ChatBubbleOutline, contentDescription = title) },
leadingIcon = { Icon(imageVector = Icons.Rounded.ChatBubbleOutline, contentDescription = title) },
)
}
@ -859,7 +859,7 @@ private fun FilteredMessagesMenuItem(showFiltered: Boolean, count: Int, onDismis
},
leadingIcon = {
Icon(
imageVector = if (showFiltered) Icons.Default.VisibilityOff else Icons.Default.Visibility,
imageVector = if (showFiltered) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility,
contentDescription = title,
)
},

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.messaging
import androidx.compose.foundation.layout.Arrangement
@ -35,10 +34,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.DragHandle
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.FastForward
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
@ -148,7 +147,7 @@ fun QuickChatScreen(
onClick = { showActionDialog = QuickChatAction(position = actions.size) },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(Res.string.add))
Icon(imageVector = Icons.Rounded.Add, contentDescription = stringResource(Res.string.add))
}
}
}
@ -231,9 +230,9 @@ private fun EditQuickChatDialog(
val (text, icon) =
if (isInstant) {
Res.string.quick_chat_instant to Icons.Default.FastForward
Res.string.quick_chat_instant to Icons.Rounded.FastForward
} else {
Res.string.quick_chat_append to Icons.Default.Add
Res.string.quick_chat_append to Icons.Rounded.Add
}
Row(verticalAlignment = Alignment.CenterVertically) {
@ -338,7 +337,7 @@ private fun QuickChatItem(
leadingContent = {
if (action.mode == QuickChatAction.Mode.Instant) {
Icon(
imageVector = Icons.Default.FastForward,
imageVector = Icons.Rounded.FastForward,
contentDescription = stringResource(Res.string.quick_chat_instant),
)
}
@ -349,12 +348,12 @@ private fun QuickChatItem(
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { onEdit(action) }, modifier = Modifier.size(48.dp)) {
Icon(
imageVector = Icons.Default.Edit,
imageVector = Icons.Rounded.Edit,
contentDescription = stringResource(Res.string.quick_chat_edit),
)
}
Icon(
imageVector = Icons.Default.DragHandle,
imageVector = Icons.Rounded.DragHandle,
contentDescription = stringResource(Res.string.quick_chat),
)
}

View file

@ -22,7 +22,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material.icons.filled.AddReaction
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material.icons.twotone.AddLink
import androidx.compose.material.icons.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
@ -61,7 +61,7 @@ internal fun ReactionButton(onSendReaction: (String) -> Unit = {}) {
)
}
IconButton(onClick = { showEmojiPickerDialog = true }) {
Icon(imageVector = Icons.Default.AddReaction, contentDescription = stringResource(Res.string.react))
Icon(imageVector = Icons.Rounded.AddReaction, contentDescription = stringResource(Res.string.react))
}
}

View file

@ -27,11 +27,11 @@ 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.AddReaction
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Reply
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Reply
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -95,25 +95,25 @@ fun MessageActionsContent(
ListItem(
headlineContent = { Text(stringResource(Res.string.reply)) },
leadingContent = { Icon(Icons.Default.Reply, contentDescription = stringResource(Res.string.reply)) },
leadingContent = { Icon(Icons.Rounded.Reply, contentDescription = stringResource(Res.string.reply)) },
modifier = Modifier.clickable(onClick = onReply),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.copy)) },
leadingContent = { Icon(Icons.Default.ContentCopy, contentDescription = stringResource(Res.string.copy)) },
leadingContent = { Icon(Icons.Rounded.ContentCopy, contentDescription = stringResource(Res.string.copy)) },
modifier = Modifier.clickable(onClick = onCopy),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.select)) },
leadingContent = { Icon(Icons.Default.SelectAll, contentDescription = stringResource(Res.string.select)) },
leadingContent = { Icon(Icons.Rounded.SelectAll, contentDescription = stringResource(Res.string.select)) },
modifier = Modifier.clickable(onClick = onSelect),
)
ListItem(
headlineContent = { Text(stringResource(Res.string.delete)) },
leadingContent = { Icon(Icons.Default.Delete, contentDescription = stringResource(Res.string.delete)) },
leadingContent = { Icon(Icons.Rounded.Delete, contentDescription = stringResource(Res.string.delete)) },
modifier = Modifier.clickable(onClick = onDelete),
)
}
@ -146,7 +146,7 @@ private fun QuickEmojiRow(quickEmojis: List<String>, onReact: (String) -> Unit,
modifier = Modifier.size(40.dp).background(MaterialTheme.colorScheme.surfaceVariant, CircleShape),
) {
Icon(
Icons.Default.AddReaction,
Icons.Rounded.AddReaction,
contentDescription = "More reactions",
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,

View file

@ -16,6 +16,7 @@
*/
package org.meshtastic.feature.messaging.component
import android.content.ClipData
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -29,16 +30,7 @@ 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.filled.Cloud
import androidx.compose.material.icons.filled.FormatQuote
import androidx.compose.material.icons.twotone.AddLink
import androidx.compose.material.icons.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
import androidx.compose.material.icons.twotone.CloudOff
import androidx.compose.material.icons.twotone.CloudUpload
import androidx.compose.material.icons.twotone.HowToReg
import androidx.compose.material.icons.twotone.Link
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material.icons.rounded.FormatQuote
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -52,18 +44,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.entity.Reaction
import org.meshtastic.core.database.model.Message
@ -71,7 +65,6 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.filter_message_label
import org.meshtastic.core.strings.hops_away_template
import org.meshtastic.core.strings.message_delivery_status
import org.meshtastic.core.strings.reply
import org.meshtastic.core.strings.sample_message
@ -82,6 +75,14 @@ import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.emoji.EmojiPicker
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
import org.meshtastic.core.ui.icon.CloudTwoTone
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Warning
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MessageItemColors
@ -123,7 +124,8 @@ internal fun MessageItem(
),
) {
var activeSheet by remember { mutableStateOf<ActiveSheet?>(null) }
val clipboardManager = LocalClipboardManager.current
val clipboardManager = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (activeSheet != null) {
@ -143,7 +145,11 @@ internal fun MessageItem(
onMoreReactions = { activeSheet = ActiveSheet.Emoji },
onCopy = {
activeSheet = null
clipboardManager.setText(AnnotatedString(message.text))
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(ClipData.newPlainText("message", message.text)),
)
}
},
onSelect = {
activeSheet = null
@ -222,7 +228,7 @@ internal fun MessageItem(
)
if (message.viaMqtt) {
Icon(
Icons.Default.Cloud,
MeshtasticIcons.Cloud,
contentDescription = stringResource(Res.string.via_mqtt),
modifier = Modifier.size(16.dp),
)
@ -278,10 +284,21 @@ internal fun MessageItem(
Rssi(message.rssi)
}
} else {
Text(
text = stringResource(Res.string.hops_away_template, message.hopsAway),
style = MaterialTheme.typography.labelSmall,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = MeshtasticIcons.Hops,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = cardColors.contentColor.copy(alpha = 0.7f),
)
Text(
text = message.hopsAway.toString(),
style = MaterialTheme.typography.labelSmall,
)
}
}
}
if (containsBel) {
@ -341,14 +358,14 @@ private enum class ActiveSheet {
private fun MessageStatusIcon(status: MessageStatus, onClick: () -> Unit, modifier: Modifier = Modifier) {
val icon =
when (status) {
MessageStatus.RECEIVED -> Icons.TwoTone.HowToReg
MessageStatus.QUEUED -> Icons.TwoTone.CloudUpload
MessageStatus.DELIVERED -> Icons.TwoTone.CloudDone
MessageStatus.SFPP_ROUTING -> Icons.TwoTone.AddLink
MessageStatus.SFPP_CONFIRMED -> Icons.TwoTone.Link
MessageStatus.ENROUTE -> Icons.TwoTone.Cloud
MessageStatus.ERROR -> Icons.TwoTone.CloudOff
else -> Icons.TwoTone.Warning
MessageStatus.RECEIVED -> MeshtasticIcons.CloudDone
MessageStatus.QUEUED -> MeshtasticIcons.CloudSync
MessageStatus.DELIVERED -> MeshtasticIcons.CloudDone
MessageStatus.SFPP_ROUTING -> MeshtasticIcons.CloudSync
MessageStatus.SFPP_CONFIRMED -> MeshtasticIcons.CloudDone
MessageStatus.ENROUTE -> MeshtasticIcons.CloudTwoTone
MessageStatus.ERROR -> MeshtasticIcons.CloudOffTwoTone
else -> MeshtasticIcons.Warning
}
Icon(
imageVector = icon,
@ -392,7 +409,7 @@ private fun OriginalMessageSnippet(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.FormatQuote,
Icons.Rounded.FormatQuote,
contentDescription = stringResource(Res.string.reply),
modifier = Modifier.size(16.dp),
)

View file

@ -35,7 +35,7 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddReaction
import androidx.compose.material.icons.rounded.AddReaction
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -67,7 +67,6 @@ import org.meshtastic.core.model.util.getShortDateTime
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.delivery_confirmed
import org.meshtastic.core.strings.error
import org.meshtastic.core.strings.hops_away_template
import org.meshtastic.core.strings.message_delivery_status
import org.meshtastic.core.strings.message_status_enroute
import org.meshtastic.core.strings.message_status_queued
@ -77,6 +76,8 @@ import org.meshtastic.core.ui.component.BottomSheetDialog
import org.meshtastic.core.ui.component.Rssi
import org.meshtastic.core.ui.component.Snr
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.messaging.DeliveryInfo
import org.meshtastic.proto.MeshProtos
@ -186,7 +187,7 @@ private fun AddReactionButton(modifier: Modifier = Modifier, onSendReaction: (St
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)),
) {
Icon(
imageVector = Icons.Default.AddReaction,
imageVector = Icons.Rounded.AddReaction,
contentDescription = stringResource(Res.string.react),
modifier = Modifier.padding(6.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
@ -300,10 +301,21 @@ internal fun ReactionDialog(
Rssi(reaction.rssi)
}
} else {
Text(
text = stringResource(Res.string.hops_away_template, reaction.hopsAway),
style = MaterialTheme.typography.labelSmall,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = MeshtasticIcons.Hops,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
)
Text(
text = reaction.hopsAway.toString(),
style = MaterialTheme.typography.labelSmall,
)
}
}
}
Spacer(modifier = Modifier.weight(1f))

View file

@ -210,7 +210,7 @@ private fun PrimaryActionsRow(
IconToggleButton(checked = node.isFavorite, onCheckedChange = { onFavoriteClick() }) {
Icon(
imageVector = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
imageVector = if (node.isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = stringResource(Res.string.favorite),
tint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
)

View file

@ -18,10 +18,10 @@ package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Column
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.material.icons.rounded.ForkLeft
import androidx.compose.material.icons.rounded.Icecream
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -63,7 +63,7 @@ fun AdministrationSection(
Column {
ListItem(
text = stringResource(Res.string.request_metadata),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
trailingIcon = null,
onClick = {
onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num)))
@ -74,7 +74,7 @@ fun AdministrationSection(
ListItem(
text = stringResource(Res.string.remote_admin),
leadingIcon = Icons.Default.Settings,
leadingIcon = Icons.Rounded.Settings,
enabled = metricsState.isLocal || node.metadata != null,
) {
onAction(NodeDetailAction.Navigate(SettingsRoutes.Settings(node.num)))
@ -101,8 +101,8 @@ private fun FirmwareSection(
firmwareEdition?.let { edition ->
val icon =
when (edition) {
MeshProtos.FirmwareEdition.VANILLA -> Icons.Default.Icecream
else -> Icons.Default.ForkLeft
MeshProtos.FirmwareEdition.VANILLA -> Icons.Rounded.Icecream
else -> Icons.Rounded.ForkLeft
}
ListItem(
@ -138,7 +138,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.installed_firmware_version),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = version.substringBeforeLast("."),
copyable = true,
leadingIconTint = statusColor,
@ -149,7 +149,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.latest_stable_firmware),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""),
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusGreen,
@ -161,7 +161,7 @@ private fun FirmwareVersionItems(
ListItem(
text = stringResource(Res.string.latest_alpha_firmware),
leadingIcon = Icons.Default.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""),
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusYellow,

View file

@ -0,0 +1,47 @@
/*
* 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.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Tsunami
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.tooling.preview.PreviewLightDark
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun ChannelInfo(
channel: Int,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Tsunami,
contentDescription = "Channel",
text = channel.toString(),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun ChannelInfoPreview() {
AppTheme { ChannelInfo(channel = 2) }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.Canvas
@ -27,8 +26,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.filled.GpsFixed
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.GpsFixed
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -153,7 +152,7 @@ fun CompassSheetContent(
)
// Quick way to re-request a fresh fix without leaving the compass sheet
Button(onClick = onRequestPosition, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.exchange_position))
}
@ -190,7 +189,7 @@ private fun WarningList(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
imageVector = Icons.Rounded.ErrorOutline,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
)
@ -205,13 +204,13 @@ private fun WarningList(
if (warnings.contains(CompassWarning.NO_LOCATION_PERMISSION)) {
Button(onClick = onRequestPermission, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.compass_no_location_permission))
}
} else if (warnings.contains(CompassWarning.LOCATION_DISABLED)) {
Button(onClick = onOpenLocationSettings, modifier = Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.GpsFixed, contentDescription = null)
Icon(imageVector = Icons.Rounded.GpsFixed, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.compass_location_disabled))
}

View file

@ -19,8 +19,6 @@ package org.meshtastic.feature.node.component
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
@ -36,6 +34,8 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
internal const val COOL_DOWN_TIME_MS = 30000L
@ -147,7 +147,7 @@ fun CooldownOutlinedIconButton(
private fun CooldownOutlinedIconButtonPreview() {
AppTheme {
CooldownOutlinedIconButton(onClick = {}, cooldownTimestamp = System.currentTimeMillis() - 15000L) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
}

View file

@ -28,10 +28,10 @@ import androidx.compose.material.icons.automirrored.filled.Message
import androidx.compose.material.icons.automirrored.filled.VolumeOff
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.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
@ -170,7 +170,7 @@ private fun PrimaryActionsRow(
IconToggleButton(checked = node.isFavorite, onCheckedChange = { onFavoriteClick() }) {
Icon(
imageVector = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
imageVector = if (node.isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = stringResource(Res.string.favorite),
tint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
)

View file

@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.twotone.Verified
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.runtime.Composable
@ -78,7 +78,7 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
?: deviceHardware.displayName
ListItem(
text = stringResource(Res.string.hardware),
leadingIcon = Icons.Default.Router,
leadingIcon = Icons.Rounded.Router,
supportingText = deviceText,
copyable = true,
trailingIcon = null,

View file

@ -20,17 +20,17 @@ 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.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.BlurOn
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Height
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Scale
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -77,7 +77,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.temperature,
temperature.toTempString(isFahrenheit),
Icons.Default.Thermostat,
Icons.Rounded.Thermostat,
),
)
}
@ -86,7 +86,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.humidity,
"%.0f%%".format(relativeHumidity),
Icons.Default.WaterDrop,
Icons.Rounded.WaterDrop,
),
)
}
@ -95,7 +95,7 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.pressure,
"%.0f hPa".format(barometricPressure),
Icons.Default.Speed,
Icons.Rounded.Speed,
),
)
}
@ -104,29 +104,29 @@ internal fun EnvironmentMetrics(
VectorMetricInfo(
Res.string.gas_resistance,
"%.0f MΩ".format(gasResistance),
Icons.Default.BlurOn,
Icons.Rounded.BlurOn,
),
)
}
if (hasVoltage()) {
add(VectorMetricInfo(Res.string.voltage, "%.2fV".format(voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.voltage, "%.2fV".format(voltage), Icons.Rounded.Bolt))
}
if (hasCurrent()) {
add(VectorMetricInfo(Res.string.current, "%.1fmA".format(current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.current, "%.1fmA".format(current), Icons.Rounded.Power))
}
if (hasIaq()) add(VectorMetricInfo(Res.string.iaq, iaq.toString(), Icons.Default.Air))
if (hasIaq()) add(VectorMetricInfo(Res.string.iaq, iaq.toString(), Icons.Rounded.Air))
if (hasDistance()) {
add(
VectorMetricInfo(
Res.string.distance,
distance.toSmallDistanceString(displayUnits),
Icons.Default.Height,
Icons.Rounded.Height,
),
)
}
if (hasLux()) add(VectorMetricInfo(Res.string.lux, "%.0f lx".format(lux), Icons.Default.LightMode))
if (hasLux()) add(VectorMetricInfo(Res.string.lux, "%.0f lx".format(lux), Icons.Rounded.LightMode))
if (hasUvLux()) {
add(VectorMetricInfo(Res.string.uv_lux, "%.0f lx".format(uvLux), Icons.Default.LightMode))
add(VectorMetricInfo(Res.string.uv_lux, "%.0f lx".format(uvLux), Icons.Rounded.LightMode))
}
if (hasWindSpeed()) {
@Suppress("MagicNumber")
@ -141,7 +141,7 @@ internal fun EnvironmentMetrics(
)
}
if (hasWeight()) {
add(VectorMetricInfo(Res.string.weight, "%.2f kg".format(weight), Icons.Default.Scale))
add(VectorMetricInfo(Res.string.weight, "%.2f kg".format(weight), Icons.Rounded.Scale))
}
if (hasTemperature() && hasRelativeHumidity()) {
val dewPoint = UnitConversions.calculateDewPoint(temperature, relativeHumidity)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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
@ -29,8 +28,8 @@ 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.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Link
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -77,7 +76,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
},
modifier = Modifier.weight(1f),
) {
Icon(imageVector = Icons.Default.Link, contentDescription = stringResource(Res.string.view_release))
Icon(imageVector = Icons.Rounded.Link, contentDescription = stringResource(Res.string.view_release))
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.view_release))
}
@ -93,7 +92,7 @@ fun FirmwareReleaseSheetContent(firmwareRelease: FirmwareRelease, modifier: Modi
},
modifier = Modifier.weight(1f),
) {
Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(Res.string.download))
Icon(imageVector = Icons.Rounded.Download, contentDescription = stringResource(Res.string.download))
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(Res.string.download))
}

View file

@ -0,0 +1,46 @@
/*
* 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.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CrueltyFree
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.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.hops_away
import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.CrueltyFree,
contentDescription = stringResource(Res.string.hops_away),
text = hops.toString(),
contentColor = contentColor,
)
}
@PreviewLightDark
@Composable
private fun HopsInfoPreview() {
AppTheme { HopsInfo(hops = 3) }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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
@ -28,6 +27,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.icon.Elevation
@ -41,6 +41,7 @@ fun IconInfo(
contentDescription: String,
modifier: Modifier = Modifier,
text: String? = null,
style: TextStyle = MaterialTheme.typography.labelMedium,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
content: @Composable () -> Unit = {},
) {
@ -55,7 +56,7 @@ fun IconInfo(
contentDescription = contentDescription,
tint = contentColor,
)
text?.let { Text(text = it, style = MaterialTheme.typography.labelMedium, color = contentColor) }
text?.let { Text(text = it, style = style, color = contentColor) }
content()
}
}

View file

@ -22,7 +22,7 @@ import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@ -87,7 +87,7 @@ fun LinkedCoordinatesItem(node: Node, displayUnits: DisplayUnits = DisplayUnits.
)
},
text = stringResource(Res.string.last_position_update),
leadingIcon = Icons.Default.LocationOn,
leadingIcon = Icons.Rounded.LocationOn,
supportingText = "$ago$coordinates$elevationText",
trailingContent = Icons.AutoMirrored.Rounded.KeyboardArrowRight.icon(),
onClick = {

View file

@ -30,17 +30,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.filled.CheckCircle
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Numbers
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.SignalCellularAlt
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.Work
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material.icons.rounded.Numbers
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -82,6 +72,17 @@ 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.icon.ArrowCircleUp
import org.meshtastic.core.ui.icon.ChannelUtilization
import org.meshtastic.core.ui.icon.Cloud
import org.meshtastic.core.ui.icon.History
import org.meshtastic.core.ui.icon.Hops
import org.meshtastic.core.ui.icon.KeyOff
import org.meshtastic.core.ui.icon.Lock
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.util.formatAgo
@Composable
@ -107,7 +108,7 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) {
Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.KeyOff,
imageVector = MeshtasticIcons.KeyOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer,
)
@ -129,7 +130,6 @@ private fun MismatchKeyWarning(modifier: Modifier = Modifier) {
}
}
@Suppress("LongMethod")
@Composable
private fun MainNodeDetails(node: Node) {
Column {
@ -151,19 +151,6 @@ private fun MainNodeDetails(node: Node) {
SectionDivider()
PublicKeyItem(publicKey.toByteArray())
}
if (!node.nodeStatus.isNullOrEmpty()) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Row(modifier = Modifier.fillMaxWidth()) {
InfoItem(
label = "Status",
value = node.nodeStatus!!,
icon = Icons.Default.CheckCircle,
modifier = Modifier.weight(1f),
)
}
}
}
}
@ -173,13 +160,13 @@ private fun NameAndRoleRow(node: Node) {
InfoItem(
label = stringResource(Res.string.short_name),
value = node.user.shortName.ifEmpty { "???" },
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
modifier = Modifier.weight(1f),
)
InfoItem(
label = stringResource(Res.string.role),
value = node.user.role.name,
icon = Icons.Default.Work,
icon = MeshtasticIcons.Role,
modifier = Modifier.weight(1f),
)
}
@ -191,13 +178,13 @@ private fun NodeIdentificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.node_id),
value = DataPacket.nodeNumToDefaultId(node.num),
icon = Icons.Default.Numbers,
icon = Icons.Rounded.Numbers,
modifier = Modifier.weight(1f),
)
InfoItem(
label = stringResource(Res.string.node_number),
value = node.num.toUInt().toString(),
icon = Icons.Default.Numbers,
icon = Icons.Rounded.Numbers,
modifier = Modifier.weight(1f),
)
}
@ -209,14 +196,14 @@ private fun HearsAndHopsRow(node: Node) {
InfoItem(
label = stringResource(Res.string.node_sort_last_heard),
value = formatAgo(node.lastHeard),
icon = Icons.Default.History,
icon = MeshtasticIcons.History,
modifier = Modifier.weight(1f),
)
if (node.hopsAway >= 0) {
InfoItem(
label = stringResource(Res.string.hops_away),
value = node.hopsAway.toString(),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.Hops,
modifier = Modifier.weight(1f),
)
} else {
@ -231,14 +218,14 @@ private fun UserAndUptimeRow(node: Node) {
InfoItem(
label = stringResource(Res.string.user_id),
value = node.user.id,
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
modifier = Modifier.weight(1f),
)
if (node.deviceMetrics.uptimeSeconds > 0) {
InfoItem(
label = stringResource(Res.string.uptime),
value = formatUptime(node.deviceMetrics.uptimeSeconds),
icon = Icons.Default.CheckCircle,
icon = MeshtasticIcons.ArrowCircleUp,
modifier = Modifier.weight(1f),
)
} else {
@ -254,7 +241,7 @@ private fun SignalRow(node: Node) {
InfoItem(
label = stringResource(Res.string.snr),
value = "%.1f dB".format(node.snr),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.ChannelUtilization,
modifier = Modifier.weight(1f),
)
} else {
@ -264,7 +251,7 @@ private fun SignalRow(node: Node) {
InfoItem(
label = stringResource(Res.string.rssi),
value = "%d dBm".format(node.rssi),
icon = Icons.Default.SignalCellularAlt,
icon = MeshtasticIcons.ChannelUtilization,
modifier = Modifier.weight(1f),
)
} else {
@ -280,7 +267,7 @@ private fun MqttAndVerificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.via_mqtt),
value = "Yes",
icon = Icons.Default.Cloud,
icon = MeshtasticIcons.Cloud,
modifier = Modifier.weight(1f),
)
} else {
@ -290,7 +277,7 @@ private fun MqttAndVerificationRow(node: Node) {
InfoItem(
label = stringResource(Res.string.supported),
value = "Verified",
icon = Icons.Default.Verified,
icon = MeshtasticIcons.Verified,
modifier = Modifier.weight(1f),
)
} else {
@ -327,7 +314,7 @@ private fun PublicKeyItem(publicKeyBytes: ByteArray) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Lock,
imageVector = MeshtasticIcons.Lock,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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
@ -32,8 +31,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -156,13 +155,13 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un
)
},
leadingIcon = {
Icon(Icons.Default.Search, contentDescription = stringResource(Res.string.node_filter_placeholder))
Icon(Icons.Rounded.Search, contentDescription = stringResource(Res.string.node_filter_placeholder))
},
onValueChange = onTextChange,
trailingIcon = {
if (filterText.isNotEmpty() || isFocused) {
Icon(
Icons.Default.Clear,
Icons.Rounded.Clear,
contentDescription = stringResource(Res.string.desc_node_filter_clear),
modifier =
Modifier.clickable {

View file

@ -20,12 +20,11 @@ import android.content.res.Configuration
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
@ -47,15 +46,31 @@ import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.elevation_suffix
import org.meshtastic.core.strings.unknown_username
import org.meshtastic.core.ui.component.AirQualityInfo
import org.meshtastic.core.ui.component.DistanceInfo
import org.meshtastic.core.ui.component.ElevationInfo
import org.meshtastic.core.ui.component.HardwareInfo
import org.meshtastic.core.ui.component.HumidityInfo
import org.meshtastic.core.ui.component.LastHeardInfo
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.component.NodeIdInfo
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
import org.meshtastic.core.ui.component.PaxcountInfo
import org.meshtastic.core.ui.component.PowerInfo
import org.meshtastic.core.ui.component.PressureInfo
import org.meshtastic.core.ui.component.RoleInfo
import org.meshtastic.core.ui.component.SatelliteCountInfo
import org.meshtastic.core.ui.component.SignalInfo
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.preview.NodePreviewParameterProvider
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig
@ -80,7 +95,17 @@ fun NodeItem(
val isFavorite = remember(thatNode) { thatNode.isFavorite }
val isMuted = remember(thatNode) { thatNode.isMuted }
val isIgnored = thatNode.isIgnored
val longName = thatNode.user.longName.ifEmpty { stringResource(Res.string.unknown_username) }
val originalLongName = thatNode.user.longName.ifEmpty { stringResource(Res.string.unknown_username) }
@Suppress("MagicNumber")
val longName =
remember(originalLongName) {
if (originalLongName.length > 20) {
"${originalLongName.take(20)}"
} else {
originalLongName
}
}
val isThisNode = remember(thatNode) { thisNode?.num == thatNode.num }
val system = remember(distanceUnits) { DisplayConfig.DisplayUnits.forNumber(distanceUnits) }
val distance =
@ -115,121 +140,183 @@ fun NodeItem(
}
}
Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) {
Card(modifier = modifier.fillMaxWidth(), colors = cardColors) {
Column(
modifier =
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode)
NodeItemHeader(
thatNode = thatNode,
isThisNode = isThisNode,
longName = longName,
style = style,
isIgnored = isIgnored,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = unmessageable,
connectionState = connectionState,
contentColor = contentColor,
)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
publicKey = thatNode.user.publicKey,
modifier = Modifier.size(32.dp),
)
Text(
modifier = Modifier.weight(1f),
text = longName,
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
softWrap = true,
)
LastHeardInfo(lastHeard = thatNode.lastHeard, contentColor = contentColor)
NodeStatusIcons(
isThisNode = isThisNode,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = unmessageable,
connectionState = connectionState,
NodeItemMetrics(thatNode = thatNode, distance = distance, system = system, contentColor = contentColor)
SignalInfo(node = thatNode, isThisNode = isThisNode, contentColor = contentColor)
NodeItemEnvironment(thatNode = thatNode, tempInFahrenheit = tempInFahrenheit, contentColor = contentColor)
NodeItemFooter(thatNode = thatNode, contentColor = contentColor)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun NodeItemHeader(
thatNode: Node,
isThisNode: Boolean,
longName: String,
style: FontStyle,
isIgnored: Boolean,
isFavorite: Boolean,
isMuted: Boolean,
isUnmessageable: Boolean,
connectionState: ConnectionState,
contentColor: Color,
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
mismatchKey = thatNode.mismatchKey,
publicKey = thatNode.user.publicKey,
modifier = Modifier.size(32.dp),
)
Text(
modifier = Modifier.weight(1f),
text = longName,
style = MaterialTheme.typography.titleMediumEmphasized.copy(fontStyle = style),
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
softWrap = true,
)
LastHeardInfo(lastHeard = thatNode.lastHeard, contentColor = contentColor)
NodeStatusIcons(
isThisNode = isThisNode,
isFavorite = isFavorite,
isMuted = isMuted,
isUnmessageable = isUnmessageable,
connectionState = connectionState,
contentColor = contentColor,
)
}
}
@Composable
private fun NodeItemMetrics(
thatNode: Node,
distance: String?,
system: DisplayConfig.DisplayUnits,
contentColor: Color,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
if (thatNode.batteryLevel > 0 || thatNode.voltage > 0f) {
MaterialBatteryInfo(level = thatNode.batteryLevel, voltage = thatNode.voltage, contentColor = contentColor)
} else {
Spacer(modifier = Modifier.weight(1f))
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (distance != null) {
DistanceInfo(distance = distance, contentColor = contentColor)
}
thatNode.validPosition?.let { position ->
ElevationInfo(
altitude = position.altitude,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (thatNode.batteryLevel > 0 || thatNode.voltage > 0f) {
MaterialBatteryInfo(
level = thatNode.batteryLevel,
voltage = thatNode.voltage,
contentColor = contentColor,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (distance != null) {
DistanceInfo(distance = distance, contentColor = contentColor)
}
thatNode.validPosition?.let { position ->
ElevationInfo(
altitude = position.altitude,
system = system,
suffix = stringResource(Res.string.elevation_suffix),
contentColor = contentColor,
)
val satCount = position.satsInView
if (satCount > 0) {
SatelliteCountInfo(satCount = satCount, contentColor = contentColor)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
itemVerticalAlignment = Alignment.CenterVertically,
) {
SignalInfo(node = thatNode, isThisNode = isThisNode, contentColor = contentColor)
}
val telemetryStrings = thatNode.getTelemetryStrings(tempInFahrenheit)
if (telemetryStrings.isNotEmpty()) {
Spacer(modifier = Modifier.height(2.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
telemetryStrings.forEach { telemetryString ->
Text(text = telemetryString, style = MaterialTheme.typography.bodySmall, color = contentColor)
}
}
}
if (!thatNode.nodeStatus.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = thatNode.nodeStatus!!,
style = MaterialTheme.typography.bodySmall,
color = contentColor,
maxLines = 2,
)
}
Spacer(modifier = Modifier.height(2.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val labelStyle =
if (thatNode.isUnknownUser) {
MaterialTheme.typography.labelSmall.copy(fontStyle = FontStyle.Italic)
} else {
MaterialTheme.typography.labelSmall
}
Text(text = thatNode.user.hwModel.name, style = labelStyle)
Text(text = thatNode.user.role.name, style = labelStyle)
Text(text = thatNode.user.id.ifEmpty { "???" }, style = labelStyle)
val satCount = thatNode.validPosition?.satsInView ?: 0
if (satCount > 0) {
SatelliteCountInfo(satCount = satCount, contentColor = contentColor)
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Suppress("CyclomaticComplexMethod")
private fun NodeItemEnvironment(thatNode: Node, tempInFahrenheit: Boolean, contentColor: Color) {
val env = thatNode.environmentMetrics
val pax = thatNode.paxcounter
if (thatNode.hasEnvironmentMetrics || pax.ble != 0 || pax.wifi != 0) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (pax.ble != 0 || pax.wifi != 0) {
PaxcountInfo(pax = "B:${pax.ble} W:${pax.wifi}", contentColor = contentColor)
}
if (env.temperature != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.temperature))
} else {
"%.1f°C".format(env.temperature)
}
TemperatureInfo(temp = temp, contentColor = contentColor)
}
if (env.relativeHumidity != 0f) {
HumidityInfo(humidity = "%.0f%%".format(env.relativeHumidity), contentColor = contentColor)
}
if (env.barometricPressure != 0f) {
PressureInfo(pressure = "%.1fhPa".format(env.barometricPressure), contentColor = contentColor)
}
if (env.soilTemperature != 0f) {
val temp =
if (tempInFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(env.soilTemperature))
} else {
"%.1f°C".format(env.soilTemperature)
}
SoilTemperatureInfo(temp = temp, contentColor = contentColor)
}
if (env.soilMoisture != 0 && env.soilTemperature != 0f) {
SoilMoistureInfo(moisture = "${env.soilMoisture}%", contentColor = contentColor)
}
if (env.voltage != 0f) {
PowerInfo(value = "%.2fV".format(env.voltage), contentColor = contentColor)
}
if (env.current != 0f) {
PowerInfo(value = "%.1fmA".format(env.current), contentColor = contentColor)
}
if (env.iaq != 0) {
AirQualityInfo(iaq = "${env.iaq}", contentColor = contentColor)
}
}
}
}
@Composable
private fun NodeItemFooter(thatNode: Node, contentColor: Color) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
HardwareInfo(hwModel = thatNode.user.hwModel.name, contentColor = contentColor)
RoleInfo(role = thatNode.user.role.name, contentColor = contentColor)
NodeIdInfo(id = thatNode.user.id.ifEmpty { "???" }, contentColor = contentColor)
}
}
@Composable
@Preview(showBackground = false, uiMode = Configuration.UI_MODE_NIGHT_YES)
fun NodeInfoSimplePreview() {

View file

@ -19,17 +19,9 @@ package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.rounded.NoCell
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.twotone.Cloud
import androidx.compose.material.icons.twotone.CloudDone
import androidx.compose.material.icons.twotone.CloudOff
import androidx.compose.material.icons.twotone.CloudSync
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
@ -55,6 +47,14 @@ import org.meshtastic.core.strings.favorite
import org.meshtastic.core.strings.mute_always
import org.meshtastic.core.strings.unmessageable
import org.meshtastic.core.strings.unmonitored_or_infrastructure
import org.meshtastic.core.ui.icon.CloudDone
import org.meshtastic.core.ui.icon.CloudOffTwoTone
import org.meshtastic.core.ui.icon.CloudSync
import org.meshtastic.core.ui.icon.CloudTwoTone
import org.meshtastic.core.ui.icon.Favorite
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Unmessageable
import org.meshtastic.core.ui.icon.VolumeOff
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@ -68,29 +68,33 @@ fun NodeStatusIcons(
isFavorite: Boolean,
isMuted: Boolean,
connectionState: ConnectionState,
modifier: Modifier = Modifier,
contentColor: Color = LocalContentColor.current,
) {
Row(modifier = Modifier.padding(4.dp)) {
Row(modifier = modifier.padding(4.dp)) {
if (isThisNode) {
ThisNodeStatusBadge(connectionState)
}
if (isUnmessageable) {
StatusBadge(
imageVector = Icons.Rounded.NoCell,
imageVector = MeshtasticIcons.Unmessageable,
contentDescription = Res.string.unmessageable,
tooltipText = Res.string.unmonitored_or_infrastructure,
tint = contentColor,
)
}
if (isMuted && !isThisNode) {
StatusBadge(
imageVector = Icons.AutoMirrored.Filled.VolumeOff,
imageVector = MeshtasticIcons.VolumeOff,
contentDescription = Res.string.mute_always,
tooltipText = Res.string.mute_always,
tint = contentColor,
)
}
if (isFavorite && !isThisNode) {
StatusBadge(
imageVector = Icons.Rounded.Star,
imageVector = MeshtasticIcons.Favorite,
contentDescription = Res.string.favorite,
tooltipText = Res.string.favorite,
tint = MaterialTheme.colorScheme.StatusYellow,
@ -132,7 +136,7 @@ private fun ThisNodeStatusBadge(connectionState: ConnectionState) {
@Composable
private fun ConnectedStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudDone,
imageVector = MeshtasticIcons.CloudDone,
contentDescription = stringResource(Res.string.connected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusGreen,
@ -142,7 +146,7 @@ private fun ConnectedStatusIcon() {
@Composable
private fun ConnectingStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudSync,
imageVector = MeshtasticIcons.CloudSync,
contentDescription = stringResource(Res.string.connecting),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusOrange,
@ -152,7 +156,7 @@ private fun ConnectingStatusIcon() {
@Composable
private fun DisconnectedStatusIcon() {
Icon(
imageVector = Icons.TwoTone.CloudOff,
imageVector = MeshtasticIcons.CloudOffTwoTone,
contentDescription = stringResource(Res.string.disconnected),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusRed,
@ -162,7 +166,7 @@ private fun DisconnectedStatusIcon() {
@Composable
private fun DeviceSleepStatusIcon() {
Icon(
imageVector = Icons.TwoTone.Cloud,
imageVector = MeshtasticIcons.CloudTwoTone,
contentDescription = stringResource(Res.string.device_sleeping),
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.StatusYellow,
@ -175,21 +179,19 @@ private fun StatusBadge(
imageVector: ImageVector,
contentDescription: StringResource,
tooltipText: StringResource,
tint: Color = Color.Unspecified,
tint: Color = LocalContentColor.current,
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(stringResource(tooltipText)) } },
state = rememberTooltipState(),
) {
IconButton(onClick = {}, modifier = Modifier.size(24.dp)) {
Icon(
imageVector = imageVector,
contentDescription = stringResource(contentDescription),
modifier = Modifier.size(24.dp),
tint = tint,
)
}
Icon(
imageVector = imageVector,
contentDescription = stringResource(contentDescription),
modifier = Modifier.size(24.dp),
tint = tint,
)
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.Column
@ -25,7 +24,7 @@ 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.material.icons.rounded.Save
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@ -87,7 +86,7 @@ fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modif
},
enabled = edited,
) {
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(Res.string.save))
Icon(imageVector = Icons.Rounded.Save, contentDescription = stringResource(Res.string.save))
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),

View file

@ -29,9 +29,9 @@ 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.filled.Explore
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.SocialDistance
import androidx.compose.material.icons.rounded.Explore
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.SocialDistance
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -132,7 +132,7 @@ private fun PositionMap(node: Node, distance: String?) {
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.SocialDistance, null, Modifier.size(16.dp))
Icon(Icons.Rounded.SocialDistance, null, Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text(distance, style = MaterialTheme.typography.labelLarge)
}
@ -163,7 +163,7 @@ private fun PositionActionButtons(
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
) {
Icon(Icons.Default.LocationOn, null, Modifier.size(18.dp))
Icon(Icons.Rounded.LocationOn, null, Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(
text = stringResource(Res.string.exchange_position),
@ -179,7 +179,7 @@ private fun PositionActionButtons(
modifier = Modifier.weight(COMPASS_BUTTON_WEIGHT),
shape = MaterialTheme.shapes.large,
) {
Icon(Icons.Default.Explore, null, Modifier.size(18.dp))
Icon(Icons.Rounded.Explore, null, Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(
text = stringResource(Res.string.open_compass),

View file

@ -20,8 +20,8 @@ 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.Bolt
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.Power
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -47,16 +47,16 @@ internal fun PowerMetrics(node: Node) {
buildList {
with(node.powerMetrics) {
if (ch1Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_1, "%.1fmA".format(ch1Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_1, "%.2fV".format(ch1Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_1, "%.1fmA".format(ch1Current), Icons.Rounded.Power))
}
if (ch2Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_2, "%.1fmA".format(ch2Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_2, "%.2fV".format(ch2Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_2, "%.1fmA".format(ch2Current), Icons.Rounded.Power))
}
if (ch3Voltage != 0f) {
add(VectorMetricInfo(Res.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Default.Bolt))
add(VectorMetricInfo(Res.string.channel_3, "%.1fmA".format(ch3Current), Icons.Default.Power))
add(VectorMetricInfo(Res.string.channel_3, "%.2fV".format(ch3Voltage), Icons.Rounded.Bolt))
add(VectorMetricInfo(Res.string.channel_3, "%.1fmA".format(ch3Current), Icons.Rounded.Power))
}
}
}

View file

@ -23,13 +23,6 @@ import androidx.compose.foundation.layout.Spacer
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.filled.Air
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.StackedLineChart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
@ -64,6 +57,14 @@ import org.meshtastic.core.strings.request_local_stats
import org.meshtastic.core.strings.request_telemetry
import org.meshtastic.core.strings.telemetry
import org.meshtastic.core.strings.userinfo
import org.meshtastic.core.ui.icon.AirQuality
import org.meshtastic.core.ui.icon.Chart
import org.meshtastic.core.ui.icon.Groups
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Person
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Speed
import org.meshtastic.core.ui.icon.Temperature
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
import org.meshtastic.feature.node.model.NodeDetailAction
@ -119,7 +120,7 @@ private fun rememberTelemetricFeatures(
listOf(
TelemetricFeature(
titleRes = Res.string.userinfo,
icon = Icons.Default.Person,
icon = MeshtasticIcons.Person,
requestAction = { NodeMenuAction.RequestUserInfo(it) },
),
TelemetricFeature(
@ -131,7 +132,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.neighbor_info,
icon = Icons.Default.Groups,
icon = MeshtasticIcons.Groups,
requestAction = { NodeMenuAction.RequestNeighborInfo(it) },
isVisible = { it.capabilities.canRequestNeighborInfo },
cooldownTimestamp = lastRequestNeighborsTime,
@ -145,7 +146,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = LogsType.ENVIRONMENT.titleRes,
icon = Icons.Default.Air,
icon = MeshtasticIcons.Temperature,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.ENVIRONMENT) },
logsType = LogsType.ENVIRONMENT,
content = { EnvironmentMetrics(it, metricsState.displayUnits, metricsState.isFahrenheit) },
@ -153,7 +154,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.request_air_quality_metrics,
icon = Icons.Default.Air,
icon = MeshtasticIcons.AirQuality,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.AIR_QUALITY) },
),
TelemetricFeature(
@ -166,7 +167,7 @@ private fun rememberTelemetricFeatures(
),
TelemetricFeature(
titleRes = Res.string.request_local_stats,
icon = Icons.Default.Speed,
icon = MeshtasticIcons.Speed,
requestAction = { NodeMenuAction.RequestTelemetry(it, TelemetryType.LOCAL_STATS) },
),
TelemetricFeature(
@ -226,7 +227,7 @@ private fun FeatureRow(node: Node, feature: TelemetricFeature, hasLogs: Boolean,
},
) {
Icon(
Icons.Default.StackedLineChart,
MeshtasticIcons.Chart,
contentDescription = logsDescription,
modifier = Modifier.size(IconButtonDefaults.mediumIconSize),
tint = MaterialTheme.colorScheme.primary,
@ -252,7 +253,7 @@ private fun FeatureRow(node: Node, feature: TelemetricFeature, hasLogs: Boolean,
cooldownDuration = feature.cooldownDuration,
) {
Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = requestDescription,
tint = MaterialTheme.colorScheme.primary,
)

View file

@ -0,0 +1,179 @@
/*
* 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.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Air
import androidx.compose.material.icons.rounded.ElectricBolt
import androidx.compose.material.icons.rounded.Fingerprint
import androidx.compose.material.icons.rounded.Grass
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.env_metrics_log
import org.meshtastic.core.strings.node_id
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.role
@Composable
fun TemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Thermostat,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun HumidityInfo(
humidity: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.WaterDrop,
contentDescription = stringResource(Res.string.env_metrics_log),
text = humidity,
contentColor = contentColor,
)
}
@Composable
fun SoilTemperatureInfo(
temp: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
text = temp,
contentColor = contentColor,
)
}
@Composable
fun SoilMoistureInfo(
moisture: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Grass,
contentDescription = stringResource(Res.string.env_metrics_log),
text = moisture,
contentColor = contentColor,
)
}
@Composable
fun PaxcountInfo(
pax: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.People,
contentDescription = stringResource(Res.string.pax_metrics_log),
text = pax,
contentColor = contentColor,
)
}
@Composable
fun AirQualityInfo(
iaq: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Air,
contentDescription = stringResource(Res.string.env_metrics_log),
text = iaq,
contentColor = contentColor,
)
}
@Composable
fun PowerInfo(value: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.ElectricBolt,
contentDescription = stringResource(Res.string.env_metrics_log),
text = value,
contentColor = contentColor,
)
}
@Composable
fun HardwareInfo(
hwModel: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Router,
contentDescription = "Hardware Model",
text = hwModel,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun RoleInfo(role: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Work,
contentDescription = stringResource(Res.string.role),
text = role,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}
@Composable
fun NodeIdInfo(id: String, modifier: Modifier = Modifier, contentColor: Color = MaterialTheme.colorScheme.onSurface) {
IconInfo(
modifier = modifier,
icon = Icons.Rounded.Fingerprint,
contentDescription = stringResource(Res.string.node_id),
text = id,
style = MaterialTheme.typography.labelSmall,
contentColor = contentColor,
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.metrics
import android.graphics.Paint
@ -32,7 +31,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -258,7 +257,7 @@ fun Legend(legendData: List<LegendData>, displayInfoIcon: Boolean = true, prompt
if (displayInfoIcon) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Default.Info,
imageVector = Icons.Rounded.Info,
modifier = Modifier.clickable { promptInfoDialog() },
contentDescription = stringResource(Res.string.info),
)

View file

@ -33,9 +33,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@ -78,6 +77,8 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.GraphColors.Cyan
import org.meshtastic.core.ui.theme.GraphColors.Green
@ -158,10 +159,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
actions = {
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.DEVICE) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},

View file

@ -28,8 +28,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -75,6 +73,8 @@ import org.meshtastic.core.ui.component.IndoorAirQuality
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
@ -134,7 +134,7 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNa
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.ENVIRONMENT) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = null,
)
}
@ -339,27 +339,6 @@ private fun GasCompositionDisplay(envMetrics: TelemetryProtos.EnvironmentMetrics
}
}
}
// These are in a differnt proto ...
// envMetrics.co2?.let { co2 ->
// Spacer(modifier = Modifier.height(4.dp))
// Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
// Text(
// text = "%s %.0f ppm".format(stringResource(Res.string.co2), co2),
// color = MaterialTheme.colorScheme.onSurface,
// fontSize = MaterialTheme.typography.labelLarge.fontSize,
// )
// }
// }
// envMetrics.tvoc?.let { tvoc ->
// Spacer(modifier = Modifier.height(4.dp))
// Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
// Text(
// text = "%s %.0f ppb".format(stringResource(Res.string.tvoc), tvoc),
// color = MaterialTheme.colorScheme.onSurface,
// fontSize = MaterialTheme.typography.labelLarge.fontSize,
// )
// }
// }
}
@Composable

View file

@ -30,9 +30,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DataArray
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
@ -69,6 +66,9 @@ import org.meshtastic.core.strings.load_indexed
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.user_string
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.DataArray
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
@ -105,10 +105,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), o
actions = {
if (!state.isLocal) {
IconButton(onClick = { metricsViewModel.requestTelemetry(TelemetryType.HOST) }) {
Icon(
imageVector = androidx.compose.material.icons.Icons.Default.Refresh,
contentDescription = null,
)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -136,7 +133,7 @@ fun HostMetricsItem(modifier: Modifier = Modifier, telemetry: TelemetryProtos.Te
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
) {
Row(modifier = Modifier.padding(16.dp)) {
Icon(imageVector = Icons.Default.DataArray, contentDescription = null, modifier = Modifier.width(24.dp))
Icon(imageVector = MeshtasticIcons.DataArray, contentDescription = null, modifier = Modifier.width(24.dp))
Spacer(modifier = Modifier.width(16.dp))
SelectionContainer {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {

View file

@ -33,8 +33,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -69,11 +67,16 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.ble_devices
import org.meshtastic.core.strings.no_pax_metrics_logs
import org.meshtastic.core.strings.pax
import org.meshtastic.core.strings.pax_metrics_log
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.wifi_devices
import org.meshtastic.core.ui.component.IconInfo
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Paxcount
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.model.TimeFrame
import org.meshtastic.proto.PaxcountProtos
@ -229,7 +232,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
actions = {
if (!state.isLocal) {
IconButton(onClick = { metricsViewModel.requestTelemetry(TelemetryType.PAX) }) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -331,6 +334,21 @@ fun unescapeProtoString(escaped: String): ByteArray {
return out.toByteArray()
}
@Composable
fun PaxcountInfo(
pax: String,
modifier: Modifier = Modifier,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
IconInfo(
modifier = modifier,
icon = MeshtasticIcons.Paxcount,
contentDescription = stringResource(Res.string.pax_metrics_log),
text = pax,
contentColor = contentColor,
)
}
@Composable
fun PaxMetricsItem(log: MeshLog, pax: PaxcountProtos.Paxcount, dateFormat: DateFormat) {
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {

View file

@ -34,10 +34,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -80,6 +76,10 @@ import org.meshtastic.core.strings.save
import org.meshtastic.core.strings.speed
import org.meshtastic.core.strings.timestamp
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Save
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.formatPositionTime
import org.meshtastic.feature.node.detail.NodeRequestEffect
@ -157,13 +157,13 @@ private fun ActionButtons(
enabled = clearButtonEnabled,
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.clear))
Icon(imageVector = MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.clear))
Spacer(Modifier.width(8.dp))
Text(text = stringResource(Res.string.clear))
}
OutlinedButton(modifier = Modifier.weight(1f), onClick = onSave, enabled = saveButtonEnabled) {
Icon(imageVector = Icons.Default.Save, contentDescription = stringResource(Res.string.save))
Icon(imageVector = MeshtasticIcons.Save, contentDescription = stringResource(Res.string.save))
Spacer(Modifier.width(8.dp))
Text(text = stringResource(Res.string.save))
}
@ -207,7 +207,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateU
actions = {
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestPosition() }) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},

View file

@ -34,7 +34,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -161,7 +161,7 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigate
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.POWER) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = Icons.Rounded.Refresh,
contentDescription = null,
)
}

View file

@ -34,8 +34,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -74,6 +72,8 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.component.SnrAndRssi
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
@ -133,7 +133,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
if (!state.isLocal) {
IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.LOCAL_STATS) }) {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Refresh,
imageVector = MeshtasticIcons.Refresh,
contentDescription = null,
)
}

View file

@ -32,12 +32,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.PersonOff
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -92,6 +86,12 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD
import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.icon.Delete
import org.meshtastic.core.ui.icon.Group
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.PersonOff
import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
@ -163,7 +163,7 @@ fun TracerouteLogScreen(
onClick = { viewModel.requestTraceroute() },
cooldownTimestamp = lastTracerouteTime,
) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
},
@ -329,7 +329,7 @@ private fun DeleteItem(onClick: () -> Unit) {
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Delete,
imageVector = MeshtasticIcons.Delete,
contentDescription = stringResource(Res.string.delete),
tint = MaterialTheme.colorScheme.error,
)
@ -357,23 +357,23 @@ private fun TracerouteItem(icon: ImageVector, text: String, modifier: Modifier =
@Composable
private fun MeshProtos.RouteDiscovery?.getTextAndIcon(): Pair<String, ImageVector> = when {
this == null -> {
stringResource(Res.string.routing_error_no_response) to Icons.Default.PersonOff
stringResource(Res.string.routing_error_no_response) to MeshtasticIcons.PersonOff
}
// A direct route means the sender and receiver are the only two nodes in the route.
routeCount <= 2 && routeBackCount <= 2 -> { // also check routeBackCount for direct to be more robust
stringResource(Res.string.traceroute_direct) to Icons.Default.Group
stringResource(Res.string.traceroute_direct) to MeshtasticIcons.Group
}
routeCount == routeBackCount -> {
val hops = routeCount - 2
pluralStringResource(Res.plurals.traceroute_hops, hops, hops) to Icons.Default.Groups
pluralStringResource(Res.plurals.traceroute_hops, hops, hops) to MeshtasticIcons.Route
}
else -> {
// Asymmetric route
val towards = maxOf(0, routeCount - 2)
val back = maxOf(0, routeBackCount - 2)
stringResource(Res.string.traceroute_diff, towards, back) to Icons.Default.Groups
stringResource(Res.string.traceroute_diff, towards, back) to MeshtasticIcons.Route
}
}
@ -424,5 +424,5 @@ private fun TracerouteItemPreview() {
System.currentTimeMillis(),
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
)
AppTheme { TracerouteItem(icon = Icons.Default.Group, text = "$time - Direct") }
AppTheme { TracerouteItem(icon = MeshtasticIcons.Group, text = "$time - Direct") }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.metrics
import androidx.compose.foundation.layout.Arrangement
@ -24,8 +23,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -52,6 +49,8 @@ import org.meshtastic.core.strings.traceroute_outgoing_route
import org.meshtastic.core.strings.traceroute_return_route
import org.meshtastic.core.strings.traceroute_showing_nodes
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Route
import org.meshtastic.core.ui.theme.TracerouteColors
import org.meshtastic.feature.map.MapView
import org.meshtastic.feature.map.model.TracerouteOverlay
@ -172,7 +171,7 @@ private fun TracerouteNodeCount(modifier: Modifier = Modifier, shown: Int, total
private fun LegendRow(color: Color, label: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.Route,
imageVector = MeshtasticIcons.Route,
contentDescription = null,
tint = color,
modifier = Modifier.padding(end = 8.dp).size(18.dp),

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,19 +14,16 @@
* 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.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.material.icons.rounded.ChargingStation
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Map
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.NodeDetailRoutes
@ -41,15 +38,18 @@ import org.meshtastic.core.strings.position_log
import org.meshtastic.core.strings.power_metrics_log
import org.meshtastic.core.strings.sig_metrics_log
import org.meshtastic.core.strings.traceroute_log
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Paxcount
import org.meshtastic.core.ui.icon.Route
enum class LogsType(val titleRes: StringResource, val icon: ImageVector, val routeFactory: (Int) -> Route) {
DEVICE(Res.string.device_metrics_log, Icons.Default.ChargingStation, { NodeDetailRoutes.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Icons.Default.Map, { NodeDetailRoutes.NodeMap(it) }),
POSITIONS(Res.string.position_log, Icons.Default.LocationOn, { NodeDetailRoutes.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Icons.Default.Thermostat, { NodeDetailRoutes.EnvironmentMetrics(it) }),
SIGNAL(Res.string.sig_metrics_log, Icons.Default.SignalCellularAlt, { NodeDetailRoutes.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Icons.Default.Power, { NodeDetailRoutes.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, Icons.Default.Route, { NodeDetailRoutes.TracerouteLog(it) }),
HOST(Res.string.host_metrics_log, Icons.Default.Memory, { NodeDetailRoutes.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, Icons.Default.People, { NodeDetailRoutes.PaxMetrics(it) }),
DEVICE(Res.string.device_metrics_log, Icons.Rounded.ChargingStation, { NodeDetailRoutes.DeviceMetrics(it) }),
NODE_MAP(Res.string.node_map, Icons.Rounded.Map, { NodeDetailRoutes.NodeMap(it) }),
POSITIONS(Res.string.position_log, Icons.Rounded.LocationOn, { NodeDetailRoutes.PositionLog(it) }),
ENVIRONMENT(Res.string.env_metrics_log, Icons.Rounded.Thermostat, { NodeDetailRoutes.EnvironmentMetrics(it) }),
SIGNAL(Res.string.sig_metrics_log, Icons.Rounded.SignalCellularAlt, { NodeDetailRoutes.SignalMetrics(it) }),
POWER(Res.string.power_metrics_log, Icons.Rounded.Power, { NodeDetailRoutes.PowerMetrics(it) }),
TRACEROUTE(Res.string.traceroute_log, MeshtasticIcons.Route, { NodeDetailRoutes.TracerouteLog(it) }),
HOST(Res.string.host_metrics_log, Icons.Rounded.Memory, { NodeDetailRoutes.HostMetricsLog(it) }),
PAX(Res.string.pax_metrics_log, MeshtasticIcons.Paxcount, { NodeDetailRoutes.PaxMetrics(it) }),
}

View file

@ -38,6 +38,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.rounded.AppSettingsAlt
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.FilterList
import androidx.compose.material.icons.rounded.FormatPaint
import androidx.compose.material.icons.rounded.Info
@ -283,7 +284,7 @@ fun SettingsScreen(
SwitchListItem(
text = stringResource(Res.string.analytics_okay),
checked = allowed,
leadingIcon = Icons.Default.BugReport,
leadingIcon = Icons.Rounded.BugReport,
onClick = { viewModel.toggleAnalyticsAllowed() },
)
}

View file

@ -36,8 +36,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.twotone.FilterAltOff
import androidx.compose.material3.Card
@ -416,7 +416,7 @@ fun DebugMenuActions(deleteLogs: () -> Unit, modifier: Modifier = Modifier) {
var showDeleteLogsDialog by remember { mutableStateOf(false) }
IconButton(onClick = { showDeleteLogsDialog = true }, modifier = modifier.padding(4.dp)) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.debug_clear))
Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.debug_clear))
}
if (showDeleteLogsDialog) {
SimpleAlertDialog(
@ -664,7 +664,7 @@ private fun DebugMenuActionsPreview() {
)
}
IconButton(onClick = { /* Preview only */ }, modifier = Modifier.padding(4.dp)) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(Res.string.debug_clear))
Icon(imageVector = Icons.Rounded.Delete, contentDescription = stringResource(Res.string.debug_clear))
}
}
}

View file

@ -29,9 +29,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.twotone.FilterAlt
import androidx.compose.material.icons.twotone.FilterAltOff
import androidx.compose.material3.DropdownMenu
@ -103,7 +104,7 @@ fun DebugCustomFilterInput(
},
enabled = customFilterText.isNotBlank(),
) {
Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(Res.string.debug_filter_add))
Icon(imageVector = Icons.Rounded.Add, contentDescription = stringResource(Res.string.debug_filter_add))
}
}
}
@ -266,7 +267,7 @@ internal fun DebugActiveFilters(
}
IconButton(onClick = { onFilterTextsChange(emptyList()) }) {
Icon(
imageVector = Icons.Default.Clear,
imageVector = Icons.Rounded.Clear,
contentDescription = stringResource(Res.string.debug_filter_clear),
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.settings.debugging
import androidx.compose.foundation.background
@ -29,10 +28,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -83,14 +82,14 @@ internal fun DebugSearchNavigation(
)
IconButton(onClick = onPreviousMatch, enabled = searchState.hasMatches, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.KeyboardArrowUp,
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = stringResource(Res.string.debug_search_prev),
modifier = Modifier.size(16.dp),
)
}
IconButton(onClick = onNextMatch, enabled = searchState.hasMatches, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = stringResource(Res.string.debug_search_next),
modifier = Modifier.size(16.dp),
)
@ -136,7 +135,7 @@ internal fun DebugSearchBar(
if (searchState.searchText.isNotEmpty()) {
IconButton(onClick = onClearSearch, modifier = Modifier.size(32.dp)) {
Icon(
imageVector = Icons.Default.Clear,
imageVector = Icons.Rounded.Clear,
contentDescription = stringResource(Res.string.debug_search_clear),
modifier = Modifier.size(16.dp),
)

View file

@ -26,8 +26,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -156,7 +156,7 @@ private fun FilterWordsInputCard(newWord: String, onNewWordChange: (String) -> U
keyboardActions = KeyboardActions(onDone = { onAddWord() }),
)
IconButton(onClick = onAddWord) {
Icon(Icons.Default.Add, contentDescription = stringResource(Res.string.add))
Icon(Icons.Rounded.Add, contentDescription = stringResource(Res.string.add))
}
}
}
@ -184,7 +184,7 @@ private fun FilterWordItem(word: String, onRemove: () -> Unit) {
)
}
IconButton(onClick = onRemove) {
Icon(Icons.Default.Delete, contentDescription = stringResource(Res.string.delete))
Icon(Icons.Rounded.Delete, contentDescription = stringResource(Res.string.delete))
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,20 +14,19 @@
* 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.settings.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CellTower
import androidx.compose.material.icons.filled.DisplaySettings
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Router
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.CellTower
import androidx.compose.material.icons.rounded.DisplaySettings
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Power
import androidx.compose.material.icons.rounded.Router
import androidx.compose.material.icons.rounded.Security
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.Route
@ -47,54 +46,54 @@ import org.meshtastic.proto.AdminProtos
import org.meshtastic.proto.MeshProtos.DeviceMetadata
enum class ConfigRoute(val title: StringResource, val route: Route, val icon: ImageVector?, val type: Int = 0) {
USER(Res.string.user, SettingsRoutes.User, Icons.Default.Person, 0),
USER(Res.string.user, SettingsRoutes.User, Icons.Rounded.Person, 0),
CHANNELS(Res.string.channels, SettingsRoutes.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE(
Res.string.device,
SettingsRoutes.Device,
Icons.Default.Router,
Icons.Rounded.Router,
AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE,
),
POSITION(
Res.string.position,
SettingsRoutes.Position,
Icons.Default.LocationOn,
Icons.Rounded.LocationOn,
AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE,
),
POWER(
Res.string.power,
SettingsRoutes.Power,
Icons.Default.Power,
Icons.Rounded.Power,
AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE,
),
NETWORK(
Res.string.network,
SettingsRoutes.Network,
Icons.Default.Wifi,
Icons.Rounded.Wifi,
AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE,
),
DISPLAY(
Res.string.display,
SettingsRoutes.Display,
Icons.Default.DisplaySettings,
Icons.Rounded.DisplaySettings,
AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE,
),
LORA(
Res.string.lora,
SettingsRoutes.LoRa,
Icons.Default.CellTower,
Icons.Rounded.CellTower,
AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE,
),
BLUETOOTH(
Res.string.bluetooth,
SettingsRoutes.Bluetooth,
Icons.Default.Bluetooth,
Icons.Rounded.Bluetooth,
AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE,
),
SECURITY(
Res.string.security,
SettingsRoutes.Security,
Icons.Default.Security,
Icons.Rounded.Security,
AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE,
),
;

View file

@ -30,6 +30,16 @@ import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material.icons.filled.SettingsRemote
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.rounded.Cloud
import androidx.compose.material.icons.rounded.DataUsage
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.PermScanWifi
import androidx.compose.material.icons.rounded.Sensors
import androidx.compose.material.icons.rounded.SettingsRemote
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.Usb
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.navigation.Route
@ -56,19 +66,19 @@ enum class ModuleRoute(val title: StringResource, val route: Route, val icon: Im
MQTT(
Res.string.mqtt,
SettingsRoutes.MQTT,
Icons.Default.Cloud,
Icons.Rounded.Cloud,
AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE,
),
SERIAL(
Res.string.serial,
SettingsRoutes.Serial,
Icons.Default.Usb,
Icons.Rounded.Usb,
AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE,
),
EXT_NOTIFICATION(
Res.string.external_notification,
SettingsRoutes.ExtNotification,
Icons.Default.Notifications,
Icons.Rounded.Notifications,
AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE,
),
STORE_FORWARD(
@ -80,13 +90,13 @@ enum class ModuleRoute(val title: StringResource, val route: Route, val icon: Im
RANGE_TEST(
Res.string.range_test,
SettingsRoutes.RangeTest,
Icons.Default.Speed,
Icons.Rounded.Speed,
AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE,
),
TELEMETRY(
Res.string.telemetry,
SettingsRoutes.Telemetry,
Icons.Default.DataUsage,
Icons.Rounded.DataUsage,
AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE,
),
CANNED_MESSAGE(
@ -104,31 +114,31 @@ enum class ModuleRoute(val title: StringResource, val route: Route, val icon: Im
REMOTE_HARDWARE(
Res.string.remote_hardware,
SettingsRoutes.RemoteHardware,
Icons.Default.SettingsRemote,
Icons.Rounded.SettingsRemote,
AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE,
),
NEIGHBOR_INFO(
Res.string.neighbor_info,
SettingsRoutes.NeighborInfo,
Icons.Default.People,
Icons.Rounded.People,
AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE,
),
AMBIENT_LIGHTING(
Res.string.ambient_lighting,
SettingsRoutes.AmbientLighting,
Icons.Default.LightMode,
Icons.Rounded.LightMode,
AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE,
),
DETECTION_SENSOR(
Res.string.detection_sensor,
SettingsRoutes.DetectionSensor,
Icons.Default.Sensors,
Icons.Rounded.Sensors,
AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE,
),
PAXCOUNTER(
Res.string.paxcounter,
SettingsRoutes.Paxcounter,
Icons.Default.PermScanWifi,
Icons.Rounded.PermScanWifi,
AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE,
),
STATUS_MESSAGE(

View file

@ -26,11 +26,13 @@ import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.PowerSettingsNew
import androidx.compose.material.icons.rounded.RestartAlt
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material.icons.rounded.Storage
import androidx.compose.material.icons.rounded.SystemUpdate
import androidx.compose.material.icons.rounded.Upload
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
@ -143,13 +145,13 @@ fun RadioConfigItemList(
ListItem(
text = stringResource(Res.string.import_configuration),
leadingIcon = Icons.Default.Download,
leadingIcon = Icons.Rounded.Download,
enabled = enabled,
onClick = onImport,
)
ListItem(
text = stringResource(Res.string.export_configuration),
leadingIcon = Icons.Default.Upload,
leadingIcon = Icons.Rounded.Upload,
enabled = enabled,
onClick = onExport,
)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@ -14,7 +14,6 @@
* 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.settings.radio.component
import android.media.MediaPlayer
@ -25,8 +24,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.rounded.FolderOpen
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -289,7 +288,7 @@ fun ExternalNotificationConfigScreen(
Row {
IconButton(onClick = { launcher.launch("*/*") }, enabled = state.connected) {
Icon(
Icons.Default.FolderOpen,
Icons.Rounded.FolderOpen,
contentDescription = stringResource(Res.string.import_label),
)
}
@ -313,7 +312,7 @@ fun ExternalNotificationConfigScreen(
},
enabled = state.connected,
) {
Icon(Icons.Default.PlayArrow, contentDescription = stringResource(Res.string.play))
Icon(Icons.Rounded.PlayArrow, contentDescription = stringResource(Res.string.play))
}
}
},