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