Improve top-level nav icons/labels (#2790)

This commit is contained in:
Phil Oliver 2025-08-22 14:02:08 -04:00 committed by GitHub
parent a885f68ecf
commit 268be3e4f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 899 additions and 68 deletions

View file

@ -34,16 +34,9 @@ import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.twotone.Chat
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.Contactless
import androidx.compose.material.icons.twotone.Map
import androidx.compose.material.icons.twotone.People
import androidx.compose.material.icons.rounded.QrCode2
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
@ -86,6 +79,7 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.android.AddNavigationTracking
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.setAttributes
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.Node
@ -106,10 +100,14 @@ import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.icons.Map
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.icons.Messages
import com.geeksville.mesh.ui.common.icons.Nodes
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusBlue
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.components.TopLevelNavIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -119,11 +117,11 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, ContactsRoutes.ContactsGraph),
Nodes(R.string.nodes, Icons.TwoTone.People, NodesRoutes.NodesGraph),
Map(R.string.map, Icons.TwoTone.Map, MapRoutes.Map),
Channels(R.string.channels, Icons.TwoTone.Contactless, ChannelsRoutes.ChannelsGraph),
Connections(R.string.connections, Icons.TwoTone.CloudOff, ConnectionsRoutes.ConnectionsGraph),
Messages(R.string.contacts, MeshtasticIcons.Messages, ContactsRoutes.ContactsGraph),
Nodes(R.string.nodes, MeshtasticIcons.Nodes, NodesRoutes.NodesGraph),
Map(R.string.map, MeshtasticIcons.Map, MapRoutes.Map),
Share(R.string.bottom_nav_share, Icons.Rounded.QrCode2, ChannelsRoutes.ChannelsGraph),
Connections(R.string.connections, Icons.Rounded.Wifi, ConnectionsRoutes.ConnectionsGraph),
;
companion object {
@ -147,6 +145,7 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
fun MainScreen(
uIViewModel: UIViewModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(),
onAction: (MainMenuAction) -> Unit,
) {
val navController = rememberNavController()
@ -226,6 +225,9 @@ fun MainScreen(
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
// State for determining the connection type icon to display
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
// State for managing the glow animation around the Connections icon
var currentGlowColor by remember { mutableStateOf(Color.Transparent) }
val animatedGlowAlpha = remember { Animatable(0f) }
@ -272,7 +274,11 @@ fun MainScreen(
PlainTooltip {
Text(
if (isConnectionsRoute) {
connectionState.getTooltipString()
when (connectionState) {
ConnectionState.CONNECTED -> stringResource(R.string.connected)
ConnectionState.DEVICE_SLEEP -> stringResource(R.string.device_sleeping)
ConnectionState.DISCONNECTED -> stringResource(R.string.disconnected)
}
} else {
stringResource(id = destination.label)
},
@ -313,7 +319,9 @@ fun MainScreen(
} else {
Modifier
}
Box(modifier = iconModifier) { TopLevelNavIcon(destination, connectionState) }
Box(modifier = iconModifier) {
TopLevelNavIcon(destination, connectionState, DeviceType.fromAddress(selectedDevice))
}
}
},
selected = isSelected,
@ -380,25 +388,6 @@ fun MainScreen(
}
}
@Composable
private fun TopLevelNavIcon(destination: TopLevelDestination, connectionState: ConnectionState) {
val iconTint =
when {
destination == TopLevelDestination.Connections -> connectionState.getConnectionColor()
else -> LocalContentColor.current
}
Icon(
imageVector =
if (destination == TopLevelDestination.Connections) {
connectionState.getConnectionIcon()
} else {
destination.icon
},
contentDescription = stringResource(id = destination.label),
tint = iconTint,
)
}
@Composable
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun VersionChecks(viewModel: UIViewModel) {
@ -483,23 +472,3 @@ private fun VersionChecks(viewModel: UIViewModel) {
}
}
}
@Composable
private fun ConnectionState.getConnectionColor(): Color = when (this) {
ConnectionState.CONNECTED -> colorScheme.StatusGreen
ConnectionState.DEVICE_SLEEP -> colorScheme.StatusYellow
ConnectionState.DISCONNECTED -> colorScheme.StatusRed
}
private fun ConnectionState.getConnectionIcon(): ImageVector = when (this) {
ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone
ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload
ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff
}
@Composable
private fun ConnectionState.getTooltipString(): String = when (this) {
ConnectionState.CONNECTED -> stringResource(R.string.connected)
ConnectionState.DEVICE_SLEEP -> stringResource(R.string.device_sleeping)
ConnectionState.DISCONNECTED -> stringResource(R.string.disconnected)
}

View file

@ -42,6 +42,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.icons.BatteryEmpty
import com.geeksville.mesh.ui.common.icons.BatteryUnknown
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
@ -72,7 +73,7 @@ fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) {
} else if (level < 0) {
Icon(
modifier = Modifier.size(SIZE_ICON.dp),
imageVector = BatteryUnknown,
imageVector = MeshtasticIcons.BatteryUnknown,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
@ -104,7 +105,7 @@ fun MaterialBatteryInfo(modifier: Modifier = Modifier, level: Int) {
size = Size(fillWidth, availableHeight),
)
},
imageVector = BatteryEmpty,
imageVector = MeshtasticIcons.BatteryEmpty,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)

View file

@ -27,9 +27,9 @@ import androidx.compose.ui.unit.dp
* This is from Material Symbols.
*
* @see
* [battery_android_0](https://fonts.google.com/icons?selected=Material+Symbols+Outlined:battery_android_0:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android)
* [battery_android_0](https://fonts.google.com/icons?icon.query=battery+android+0&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val BatteryEmpty: ImageVector
val MeshtasticIcons.BatteryEmpty: ImageVector
get() {
if (batteryEmpty != null) {
return batteryEmpty!!
@ -99,9 +99,9 @@ private var batteryEmpty: ImageVector? = null
* This is from Material Symbols.
*
* @see
* [battery_android_question](https://fonts.google.com/icons?selected=Material+Symbols+Outlined:battery_android_question:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=battery&icon.set=Material+Symbols&icon.size=24&icon.color=%23000000&icon.platform=android)
* [battery_android_question](https://fonts.google.com/icons?icon.query=battery+android+question&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val BatteryUnknown: ImageVector
val MeshtasticIcons.BatteryUnknown: ImageVector
get() {
if (batteryUnknown != null) {
return batteryUnknown!!

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [router](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:router:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=router&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Device: ImageVector
get() {
if (device != null) {
return device!!
}
device =
ImageVector.Builder(
name = "Outlined.Device",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color(0xFFE3E3E3))) {
moveTo(200f, 840f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(120f, 760f)
verticalLineToRelative(-160f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(200f, 520f)
horizontalLineToRelative(400f)
verticalLineToRelative(-120f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(640f, 360f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(680f, 400f)
verticalLineToRelative(120f)
horizontalLineToRelative(80f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(840f, 600f)
verticalLineToRelative(160f)
quadToRelative(0f, 33f, -23.5f, 56.5f)
reflectiveQuadTo(760f, 840f)
lineTo(200f, 840f)
close()
moveTo(200f, 760f)
horizontalLineToRelative(560f)
verticalLineToRelative(-160f)
lineTo(200f, 600f)
verticalLineToRelative(160f)
close()
moveTo(280f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(320f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(280f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(240f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(280f, 720f)
close()
moveTo(420f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(460f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(420f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(380f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(420f, 720f)
close()
moveTo(560f, 720f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(600f, 680f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(560f, 640f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(520f, 680f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(560f, 720f)
close()
moveTo(640f, 300f)
quadToRelative(-11f, 0f, -20f, 2f)
reflectiveQuadToRelative(-18f, 6f)
quadToRelative(-16f, 7f, -32.5f, 6f)
reflectiveQuadTo(541f, 301f)
quadToRelative(-12f, -12f, -11.5f, -29f)
reflectiveQuadToRelative(14.5f, -25f)
quadToRelative(21f, -13f, 45.5f, -20f)
reflectiveQuadToRelative(50.5f, -7f)
quadToRelative(27f, 0f, 51f, 7f)
reflectiveQuadToRelative(45f, 20f)
quadToRelative(14f, 8f, 14.5f, 25f)
reflectiveQuadTo(739f, 301f)
quadToRelative(-12f, 12f, -29f, 13f)
reflectiveQuadToRelative(-33f, -6f)
quadToRelative(-8f, -4f, -17.5f, -6f)
reflectiveQuadToRelative(-19.5f, -2f)
close()
moveTo(640f, 160f)
quadToRelative(-39f, 0f, -74.5f, 11.5f)
reflectiveQuadTo(500f, 205f)
quadToRelative(-14f, 10f, -30.5f, 9f)
reflectiveQuadTo(442f, 202f)
quadToRelative(-12f, -12f, -12f, -28f)
reflectiveQuadToRelative(13f, -26f)
quadToRelative(41f, -32f, 91f, -50f)
reflectiveQuadToRelative(106f, -18f)
quadToRelative(56f, 0f, 106f, 18f)
reflectiveQuadToRelative(91f, 50f)
quadToRelative(13f, 10f, 13f, 26f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-11f, 11f, -27.5f, 12f)
reflectiveQuadToRelative(-30.5f, -9f)
quadToRelative(-30f, -22f, -65.5f, -33.5f)
reflectiveQuadTo(640f, 160f)
close()
moveTo(200f, 760f)
verticalLineToRelative(-160f)
verticalLineToRelative(160f)
close()
}
}
.build()
return device!!
}
private var device: ImageVector? = null

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [map](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:map:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=map&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Map: ImageVector
get() {
if (map != null) {
return map!!
}
map =
ImageVector.Builder(
name = "Outlined.Map",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveToRelative(574f, 831f)
lineToRelative(-214f, -75f)
lineToRelative(-186f, 72f)
quadToRelative(-10f, 4f, -19.5f, 2.5f)
reflectiveQuadTo(137f, 824f)
quadToRelative(-8f, -5f, -12.5f, -13.5f)
reflectiveQuadTo(120f, 791f)
verticalLineToRelative(-561f)
quadToRelative(0f, -13f, 7.5f, -23f)
reflectiveQuadToRelative(20.5f, -15f)
lineToRelative(186f, -63f)
quadToRelative(6f, -2f, 12.5f, -3f)
reflectiveQuadToRelative(13.5f, -1f)
quadToRelative(7f, 0f, 13.5f, 1f)
reflectiveQuadToRelative(12.5f, 3f)
lineToRelative(214f, 75f)
lineToRelative(186f, -72f)
quadToRelative(10f, -4f, 19.5f, -2.5f)
reflectiveQuadTo(823f, 136f)
quadToRelative(8f, 5f, 12.5f, 13.5f)
reflectiveQuadTo(840f, 169f)
verticalLineToRelative(561f)
quadToRelative(0f, 13f, -7.5f, 23f)
reflectiveQuadTo(812f, 768f)
lineToRelative(-186f, 63f)
quadToRelative(-6f, 2f, -12.5f, 3f)
reflectiveQuadToRelative(-13.5f, 1f)
quadToRelative(-7f, 0f, -13.5f, -1f)
reflectiveQuadToRelative(-12.5f, -3f)
close()
moveTo(560f, 742f)
verticalLineToRelative(-468f)
lineToRelative(-160f, -56f)
verticalLineToRelative(468f)
lineToRelative(160f, 56f)
close()
moveTo(640f, 742f)
lineTo(760f, 702f)
verticalLineToRelative(-474f)
lineToRelative(-120f, 46f)
verticalLineToRelative(468f)
close()
moveTo(200f, 732f)
lineTo(320f, 686f)
verticalLineToRelative(-468f)
lineToRelative(-120f, 40f)
verticalLineToRelative(474f)
close()
moveTo(640f, 274f)
verticalLineToRelative(468f)
verticalLineToRelative(-468f)
close()
moveTo(320f, 218f)
verticalLineToRelative(468f)
verticalLineToRelative(-468f)
close()
}
}
.build()
return map!!
}
private var map: ImageVector? = null

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
object MeshtasticIcons

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [forum](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:forum:FILL@0;wght@400;GRAD@0;opsz@24&icon.query=forum&icon.size=24&icon.color=%23e3e3e3&icon.platform=android&icon.style=Rounded)
*/
val MeshtasticIcons.Messages: ImageVector
get() {
if (messages != null) {
return messages!!
}
messages =
ImageVector.Builder(
name = "Outlined.Messages",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(840f, 824f)
quadToRelative(-8f, 0f, -15f, -3f)
reflectiveQuadToRelative(-13f, -9f)
lineToRelative(-92f, -92f)
lineTo(320f, 720f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(240f, 640f)
verticalLineToRelative(-40f)
horizontalLineToRelative(440f)
quadToRelative(33f, 0f, 56.5f, -23.5f)
reflectiveQuadTo(760f, 520f)
verticalLineToRelative(-280f)
horizontalLineToRelative(40f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(880f, 320f)
verticalLineToRelative(463f)
quadToRelative(0f, 18f, -12f, 29.5f)
reflectiveQuadTo(840f, 824f)
close()
moveTo(160f, 487f)
lineToRelative(47f, -47f)
horizontalLineToRelative(393f)
verticalLineToRelative(-280f)
lineTo(160f, 160f)
verticalLineToRelative(327f)
close()
moveTo(120f, 624f)
quadToRelative(-16f, 0f, -28f, -11.5f)
reflectiveQuadTo(80f, 583f)
verticalLineToRelative(-423f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(160f, 80f)
horizontalLineToRelative(440f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(680f, 160f)
verticalLineToRelative(280f)
quadToRelative(0f, 33f, -23.5f, 56.5f)
reflectiveQuadTo(600f, 520f)
lineTo(240f, 520f)
lineToRelative(-92f, 92f)
quadToRelative(-6f, 6f, -13f, 9f)
reflectiveQuadToRelative(-15f, 3f)
close()
moveTo(160f, 440f)
verticalLineToRelative(-280f)
verticalLineToRelative(280f)
close()
}
}
.build()
return messages!!
}
private var messages: ImageVector? = null

View file

@ -0,0 +1,165 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [router_off](https://fonts.google.com/icons?icon.query=router+off&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.NoDevice: ImageVector
get() {
if (noDevice != null) {
return noDevice!!
}
noDevice =
ImageVector.Builder(
name = "Outlined.NoDevice",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(806f, 692f)
lineTo(600f, 486f)
verticalLineToRelative(-86f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(640f, 360f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(680f, 400f)
verticalLineToRelative(120f)
horizontalLineToRelative(80f)
quadToRelative(33f, 0f, 56.5f, 23.5f)
reflectiveQuadTo(840f, 600f)
verticalLineToRelative(78f)
quadToRelative(0f, 14f, -12f, 19f)
reflectiveQuadToRelative(-22f, -5f)
close()
moveTo(200f, 760f)
horizontalLineToRelative(446f)
lineTo(486f, 600f)
lineTo(200f, 600f)
verticalLineToRelative(160f)
close()
moveTo(200f, 840f)
quadToRelative(-33f, 0f, -56.5f, -23.5f)
reflectiveQuadTo(120f, 760f)
verticalLineToRelative(-160f)
quadToRelative(0f, -33f, 23.5f, -56.5f)
reflectiveQuadTo(200f, 520f)
horizontalLineToRelative(206f)
lineTo(83f, 197f)
quadToRelative(-12f, -12f, -12f, -28.5f)
reflectiveQuadTo(83f, 140f)
quadToRelative(12f, -12f, 28.5f, -12f)
reflectiveQuadToRelative(28.5f, 12f)
lineToRelative(680f, 680f)
quadToRelative(12f, 12f, 12f, 28f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-12f, 12f, -28.5f, 12f)
reflectiveQuadTo(763f, 876f)
lineToRelative(-37f, -36f)
lineTo(200f, 840f)
close()
moveTo(280f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(240f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(280f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(320f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(280f, 720f)
close()
moveTo(420f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(380f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(420f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(460f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(420f, 720f)
close()
moveTo(560f, 720f)
quadToRelative(-17f, 0f, -28.5f, -11.5f)
reflectiveQuadTo(520f, 680f)
quadToRelative(0f, -17f, 11.5f, -28.5f)
reflectiveQuadTo(560f, 640f)
quadToRelative(17f, 0f, 28.5f, 11.5f)
reflectiveQuadTo(600f, 680f)
quadToRelative(0f, 17f, -11.5f, 28.5f)
reflectiveQuadTo(560f, 720f)
close()
moveTo(200f, 760f)
verticalLineToRelative(-160f)
verticalLineToRelative(160f)
close()
moveTo(640f, 300f)
quadToRelative(-11f, 0f, -20f, 2f)
reflectiveQuadToRelative(-18f, 6f)
quadToRelative(-16f, 7f, -32.5f, 6f)
reflectiveQuadTo(541f, 301f)
quadToRelative(-12f, -12f, -11.5f, -29f)
reflectiveQuadToRelative(14.5f, -25f)
quadToRelative(21f, -13f, 45.5f, -20f)
reflectiveQuadToRelative(50.5f, -7f)
quadToRelative(27f, 0f, 51f, 7f)
reflectiveQuadToRelative(45f, 20f)
quadToRelative(14f, 8f, 14.5f, 25f)
reflectiveQuadTo(739f, 301f)
quadToRelative(-12f, 12f, -29f, 13f)
reflectiveQuadToRelative(-33f, -6f)
quadToRelative(-8f, -4f, -17.5f, -6f)
reflectiveQuadToRelative(-19.5f, -2f)
close()
moveTo(640f, 160f)
quadToRelative(-39f, 0f, -74.5f, 11.5f)
reflectiveQuadTo(500f, 205f)
quadToRelative(-14f, 10f, -30.5f, 9f)
reflectiveQuadTo(442f, 202f)
quadToRelative(-12f, -12f, -12f, -28f)
reflectiveQuadToRelative(13f, -26f)
quadToRelative(41f, -32f, 91f, -50f)
reflectiveQuadToRelative(106f, -18f)
quadToRelative(56f, 0f, 106f, 18f)
reflectiveQuadToRelative(91f, 50f)
quadToRelative(13f, 10f, 13f, 26f)
reflectiveQuadToRelative(-12f, 28f)
quadToRelative(-11f, 11f, -27.5f, 12f)
reflectiveQuadToRelative(-30.5f, -9f)
quadToRelative(-30f, -22f, -65.5f, -33.5f)
reflectiveQuadTo(640f, 160f)
close()
}
}
.build()
return noDevice!!
}
private var noDevice: ImageVector? = null

View file

@ -0,0 +1,166 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
/**
* This is from Material Symbols.
*
* @see
* [graph_3](https://fonts.google.com/icons?icon.query=graph+3&icon.size=24&icon.color=%23e3e3e3&icon.style=Rounded)
*/
val MeshtasticIcons.Nodes: ImageVector
get() {
if (nodes != null) {
return nodes!!
}
nodes =
ImageVector.Builder(
name = "Outlined.Nodes",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 960f,
viewportHeight = 960f,
)
.apply {
path(fill = SolidColor(Color.Black)) {
moveTo(480f, 880f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -5f, 0.5f, -11f)
reflectiveQuadToRelative(1.5f, -11f)
lineToRelative(-83f, -47f)
quadToRelative(-16f, 14f, -36f, 21.5f)
reflectiveQuadToRelative(-43f, 7.5f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(24f, 0f, 45f, 9f)
reflectiveQuadToRelative(38f, 25f)
lineToRelative(119f, -60f)
quadToRelative(-3f, -23f, 2.5f, -45f)
reflectiveQuadToRelative(19.5f, -41f)
lineToRelative(-34f, -52f)
quadToRelative(-7f, 2f, -14.5f, 3f)
reflectiveQuadToRelative(-15.5f, 1f)
quadToRelative(-50f, 0f, -85f, -35f)
reflectiveQuadToRelative(-35f, -85f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 20f, -6.5f, 38.5f)
reflectiveQuadTo(456f, 272f)
lineToRelative(35f, 52f)
quadToRelative(8f, -2f, 15f, -3f)
reflectiveQuadToRelative(15f, -1f)
quadToRelative(17f, 0f, 32f, 4f)
reflectiveQuadToRelative(29f, 12f)
lineToRelative(66f, -54f)
quadToRelative(-4f, -10f, -6f, -20.5f)
reflectiveQuadToRelative(-2f, -21.5f)
quadToRelative(0f, -50f, 35f, -85f)
reflectiveQuadToRelative(85f, -35f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
quadToRelative(-17f, 0f, -32f, -4.5f)
reflectiveQuadTo(699f, 343f)
lineToRelative(-66f, 55f)
quadToRelative(4f, 10f, 6f, 20.5f)
reflectiveQuadToRelative(2f, 21.5f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
quadToRelative(-24f, 0f, -45.5f, -9f)
reflectiveQuadTo(437f, 526f)
lineToRelative(-118f, 59f)
quadToRelative(2f, 9f, 1.5f, 18f)
reflectiveQuadToRelative(-2.5f, 18f)
lineToRelative(84f, 48f)
quadToRelative(16f, -14f, 35.5f, -21.5f)
reflectiveQuadTo(480f, 640f)
quadToRelative(50f, 0f, 85f, 35f)
reflectiveQuadToRelative(35f, 85f)
quadToRelative(0f, 50f, -35f, 85f)
reflectiveQuadToRelative(-85f, 35f)
close()
moveTo(200f, 640f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(240f, 600f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(200f, 560f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(160f, 600f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(200f, 640f)
close()
moveTo(360f, 240f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(400f, 200f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(360f, 160f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(320f, 200f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(360f, 240f)
close()
moveTo(480f, 800f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(520f, 760f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(480f, 720f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(440f, 760f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(480f, 800f)
close()
moveTo(520f, 480f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(560f, 440f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(520f, 400f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(480f, 440f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(520f, 480f)
close()
moveTo(760f, 280f)
quadToRelative(17f, 0f, 28.5f, -11.5f)
reflectiveQuadTo(800f, 240f)
quadToRelative(0f, -17f, -11.5f, -28.5f)
reflectiveQuadTo(760f, 200f)
quadToRelative(-17f, 0f, -28.5f, 11.5f)
reflectiveQuadTo(720f, 240f)
quadToRelative(0f, 17f, 11.5f, 28.5f)
reflectiveQuadTo(760f, 280f)
close()
}
}
.build()
return nodes!!
}
private var nodes: ImageVector? = null

View file

@ -602,7 +602,7 @@ private tailrec fun Context.findActivity(): Activity = when (this) {
else -> error("No activity found")
}
private enum class DeviceType {
enum class DeviceType {
BLE,
TCP,
USB,

View file

@ -0,0 +1,147 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.connections.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.Snooze
import androidx.compose.material.icons.rounded.Usb
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.TopLevelDestination
import com.geeksville.mesh.ui.common.icons.Device
import com.geeksville.mesh.ui.common.icons.MeshtasticIcons
import com.geeksville.mesh.ui.common.icons.NoDevice
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.connections.DeviceType
@Composable
fun TopLevelNavIcon(destination: TopLevelDestination, connectionState: ConnectionState, deviceType: DeviceType?) {
if (destination == TopLevelDestination.Connections) {
ConnectionsNavIcon(connectionState = connectionState, deviceType = deviceType)
} else {
Icon(
imageVector = destination.icon,
contentDescription = stringResource(id = destination.label),
tint = LocalContentColor.current,
)
}
}
@Composable
private fun ConnectionsNavIcon(
modifier: Modifier = Modifier,
connectionState: ConnectionState,
deviceType: DeviceType?,
) {
val tint =
when (connectionState) {
ConnectionState.DISCONNECTED -> colorScheme.StatusRed
ConnectionState.DEVICE_SLEEP -> colorScheme.StatusYellow
else -> colorScheme.StatusGreen
}
val (backgroundIcon, connectionTypeIcon) =
when (connectionState) {
ConnectionState.DISCONNECTED -> MeshtasticIcons.NoDevice to null
ConnectionState.DEVICE_SLEEP -> MeshtasticIcons.Device to Icons.Rounded.Snooze
else ->
MeshtasticIcons.Device to
when (deviceType) {
DeviceType.BLE -> Icons.Rounded.Bluetooth
DeviceType.TCP -> Icons.Rounded.Wifi
DeviceType.USB -> Icons.Rounded.Usb
else -> null
}
}
val foregroundPainter = connectionTypeIcon?.let { rememberVectorPainter(it) }
Icon(
imageVector = backgroundIcon,
contentDescription = null,
tint = tint,
modifier =
modifier.drawWithContent {
drawContent()
foregroundPainter?.let {
@Suppress("MagicNumber")
val badgeSize = size.width * .45f
with(it) { draw(Size(badgeSize, badgeSize), colorFilter = ColorFilter.tint(tint)) }
}
},
)
}
class TopLevelDestinationProvider : PreviewParameterProvider<TopLevelDestination> {
override val values: Sequence<TopLevelDestination> = TopLevelDestination.entries.asSequence()
}
class ConnectionStateProvider : PreviewParameterProvider<ConnectionState> {
override val values: Sequence<ConnectionState> =
sequenceOf(ConnectionState.CONNECTED, ConnectionState.DEVICE_SLEEP, ConnectionState.DISCONNECTED)
}
class DeviceTypeProvider : PreviewParameterProvider<DeviceType> {
override val values: Sequence<DeviceType> = sequenceOf(DeviceType.BLE, DeviceType.TCP, DeviceType.USB)
}
@PreviewLightDark
@Composable
private fun TopLevelNavIconPreviewConnectionStates(
@PreviewParameter(TopLevelDestinationProvider::class) destination: TopLevelDestination,
) {
AppTheme {
TopLevelNavIcon(
destination = destination,
connectionState = ConnectionState.CONNECTED,
deviceType = DeviceType.BLE,
)
}
}
@PreviewLightDark
@Composable
private fun ConnectionsNavIconPreviewConnectionStates(
@PreviewParameter(ConnectionStateProvider::class) connectionState: ConnectionState,
) {
AppTheme { ConnectionsNavIcon(connectionState = connectionState, deviceType = DeviceType.BLE) }
}
@Preview(showBackground = true)
@Composable
private fun ConnectionsNavIconPreviewDeviceTypes(@PreviewParameter(DeviceTypeProvider::class) deviceType: DeviceType) {
ConnectionsNavIcon(connectionState = ConnectionState.CONNECTED, deviceType = deviceType)
}

View file

@ -120,7 +120,7 @@
<string name="connected_count">Connected: %1$s online</string>
<string name="ip_address">IP Address:</string>
<string name="ip_port">Port:</string>
<string name="connected">Connected to radio</string>
<string name="connected">Connected</string>
<string name="connected_to">Connected to radio (%s)</string>
<string name="not_connected">Not connected</string>
<string name="connected_sleeping">Connected to radio, but it is sleeping</string>
@ -621,10 +621,11 @@
<string name="load">Load</string>
<string name="user_string">User String</string>
<string name="navigate_into_label">Navigate Into</string>
<string name="connections">Connections</string>
<string name="map">Map</string>
<string name="contacts">Contacts</string>
<string name="connections">Connection</string>
<string name="map">Mesh Map</string>
<string name="contacts">Messages</string>
<string name="nodes">Nodes</string>
<string name="bottom_nav_share">Share</string>
<string name="set_your_region">Set your region</string>
<string name="reply">Reply</string>
<string name="map_reporting_summary">Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name.</string>