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

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