From da1932fae0f2c4f075abd7460e588253a08c2afa Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Tue, 19 Aug 2025 19:55:10 -0400
Subject: [PATCH] Extract `MainAppBar` to its own file (#2788)
---
.../java/com/geeksville/mesh/MainActivity.kt | 2 +-
.../java/com/geeksville/mesh/model/UIState.kt | 2 +-
.../main/java/com/geeksville/mesh/ui/Main.kt | 169 +----------
.../mesh/ui/common/components/MainAppBar.kt | 264 ++++++++++++++++++
.../mesh/ui/common/components/PreviewUtils.kt | 42 +++
5 files changed, 312 insertions(+), 167 deletions(-)
create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/components/PreviewUtils.kt
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index eb585a807..e241be5ae 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -49,8 +49,8 @@ import com.geeksville.mesh.android.prefs.UiPrefs
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI
-import com.geeksville.mesh.ui.MainMenuAction
import com.geeksville.mesh.ui.MainScreen
+import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index 57f60a84d..3067b2c37 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -61,7 +61,7 @@ import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceAction
-import com.geeksville.mesh.ui.MainMenuAction
+import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.util.getShortDate
import com.geeksville.mesh.util.positionToMeter
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index 7af568544..c37202079 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -15,16 +15,16 @@
* along with this program. If not, see .
*/
+@file:Suppress("MatchingDeclarationName")
+
package com.geeksville.mesh.ui
import android.Manifest
import android.os.Build
import androidx.annotation.StringRes
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -34,29 +34,21 @@ 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.filled.ArrowBack
import androidx.compose.material.icons.automirrored.twotone.Chat
-import androidx.compose.material.icons.filled.MoreVert
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.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
-import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
@@ -78,15 +70,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.res.vectorResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
-import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.BuildConfig
@@ -107,11 +96,11 @@ import com.geeksville.mesh.navigation.NavGraph
import com.geeksville.mesh.navigation.NodesRoutes
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.navigation.Route
-import com.geeksville.mesh.navigation.showLongNameTitle
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
-import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
+import com.geeksville.mesh.ui.common.components.MainAppBar
+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
@@ -119,10 +108,7 @@ 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.debug.DebugMenuActions
-import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.node.components.NodeMenuAction
-import com.geeksville.mesh.ui.radioconfig.RadioConfigMenuActions
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
@@ -494,153 +480,6 @@ private fun VersionChecks(viewModel: UIViewModel) {
}
}
-enum class MainMenuAction(@StringRes val stringRes: Int) {
- DEBUG(R.string.debug_panel),
- RADIO_CONFIG(R.string.radio_configuration),
- EXPORT_RANGETEST(R.string.save_rangetest),
- THEME(R.string.theme),
- LANGUAGE(R.string.preferences_language),
- SHOW_INTRO(R.string.intro_show),
- QUICK_CHAT(R.string.quick_chat),
- ABOUT(R.string.about),
-}
-
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
-@Suppress("LongMethod")
-@Composable
-private fun MainAppBar(
- viewModel: UIViewModel = hiltViewModel(),
- isManaged: Boolean,
- navController: NavHostController,
- modifier: Modifier = Modifier,
- onAction: (Any?) -> Unit,
-) {
- val backStackEntry by navController.currentBackStackEntryAsState()
- val currentDestination = backStackEntry?.destination
- val canNavigateBack = navController.previousBackStackEntry != null
- val navigateUp: () -> Unit = navController::navigateUp
- if (currentDestination?.hasRoute() == true) {
- return
- }
- val title by viewModel.title.collectAsStateWithLifecycle("")
- val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
- val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)
- TopAppBar(
- title = {
- val titleText =
- when {
- currentDestination == null || currentDestination.isTopLevel() ->
- stringResource(id = R.string.app_name)
-
- currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel)
-
- currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat)
-
- currentDestination.hasRoute() -> stringResource(id = R.string.share_to)
-
- currentDestination.showLongNameTitle() -> title
-
- else -> stringResource(id = R.string.app_name)
- }
- Text(
- text = titleText,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.titleLarge,
- )
- },
- subtitle = {
- if (currentDestination?.hasRoute() == true) {
- Text(text = stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount))
- }
- },
- modifier = modifier,
- navigationIcon =
- if (canNavigateBack && currentDestination?.isTopLevel() == false) {
- {
- IconButton(onClick = navigateUp) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = stringResource(id = R.string.navigate_back),
- )
- }
- }
- } else {
- {
- IconButton(enabled = false, onClick = {}) {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.app_icon),
- contentDescription = stringResource(id = R.string.application_icon),
- )
- }
- }
- },
- actions = {
- TopBarActions(
- viewModel = viewModel,
- currentDestination = currentDestination,
- isManaged = isManaged,
- onAction = onAction,
- )
- },
- )
-}
-
-@Composable
-private fun TopBarActions(
- viewModel: UIViewModel = hiltViewModel(),
- currentDestination: NavDestination?,
- isManaged: Boolean,
- onAction: (Any?) -> Unit,
-) {
- val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
- val isConnected by viewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
- AnimatedVisibility(ourNode != null && currentDestination?.isTopLevel() == true && isConnected) {
- ourNode?.let { NodeChip(node = it, isThisNode = true, isConnected = isConnected, onAction = onAction) }
- }
- currentDestination?.let {
- when {
- it.isTopLevel() -> MainMenuActions(isManaged, onAction)
-
- currentDestination.hasRoute() -> DebugMenuActions()
-
- currentDestination.hasRoute() ->
- RadioConfigMenuActions(viewModel = viewModel)
-
- else -> {}
- }
- }
-}
-
-@Composable
-private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Unit) {
- var showMenu by remember { mutableStateOf(false) }
- IconButton(onClick = { showMenu = true }) {
- Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
- }
-
- DropdownMenu(
- expanded = showMenu,
- onDismissRequest = { showMenu = false },
- modifier = Modifier.background(colorScheme.background.copy(alpha = 1f)),
- ) {
- MainMenuAction.entries.forEach { action ->
- DropdownMenuItem(
- text = { Text(stringResource(id = action.stringRes)) },
- onClick = {
- onAction(action)
- showMenu = false
- },
- enabled =
- when (action) {
- MainMenuAction.RADIO_CONFIG -> !isManaged
- else -> true
- },
- )
- }
- }
-}
-
@Composable
private fun ConnectionState.getConnectionColor(): Color = when (this) {
ConnectionState.CONNECTED -> colorScheme.StatusGreen
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
new file mode 100644
index 000000000..7a68428b5
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt
@@ -0,0 +1,264 @@
+/*
+ * 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 .
+ */
+
+package com.geeksville.mesh.ui.common.components
+
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavDestination.Companion.hasRoute
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.geeksville.mesh.R
+import com.geeksville.mesh.model.Node
+import com.geeksville.mesh.model.UIViewModel
+import com.geeksville.mesh.navigation.ContactsRoutes
+import com.geeksville.mesh.navigation.NodesRoutes
+import com.geeksville.mesh.navigation.RadioConfigRoutes
+import com.geeksville.mesh.navigation.Route
+import com.geeksville.mesh.navigation.showLongNameTitle
+import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
+import com.geeksville.mesh.ui.common.theme.AppTheme
+import com.geeksville.mesh.ui.debug.DebugMenuActions
+import com.geeksville.mesh.ui.node.components.NodeChip
+import com.geeksville.mesh.ui.radioconfig.RadioConfigMenuActions
+
+@Suppress("CyclomaticComplexMethod")
+@Composable
+fun MainAppBar(
+ modifier: Modifier = Modifier,
+ viewModel: UIViewModel = hiltViewModel(),
+ navController: NavHostController,
+ isManaged: Boolean,
+ onAction: (Any?) -> Unit,
+) {
+ val backStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = backStackEntry?.destination
+ if (currentDestination?.hasRoute() == true) {
+ return
+ }
+
+ val longTitle by viewModel.title.collectAsStateWithLifecycle("")
+ val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
+ val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)
+ val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
+ val isConnected by viewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
+
+ val title: String =
+ when {
+ currentDestination == null || currentDestination.isTopLevel() -> stringResource(id = R.string.app_name)
+
+ currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel)
+
+ currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat)
+
+ currentDestination.hasRoute() -> stringResource(id = R.string.share_to)
+
+ currentDestination.showLongNameTitle() -> longTitle
+
+ else -> stringResource(id = R.string.app_name)
+ }
+
+ val subtitle =
+ if (currentDestination?.hasRoute() == true) {
+ stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount)
+ } else {
+ null
+ }
+
+ MainAppBar(
+ modifier = modifier,
+ title = title,
+ subtitle = subtitle,
+ canNavigateUp = navController.previousBackStackEntry != null && currentDestination?.isTopLevel() == false,
+ ourNode = ourNode,
+ isConnected = isConnected,
+ showNodeChip = ourNode != null && currentDestination?.isTopLevel() == true && isConnected,
+ onNavigateUp = navController::navigateUp,
+ actions = {
+ currentDestination?.let {
+ when {
+ it.isTopLevel() -> MainMenuActions(isManaged, onAction)
+
+ currentDestination.hasRoute() -> DebugMenuActions()
+
+ currentDestination.hasRoute() ->
+ RadioConfigMenuActions(viewModel = viewModel)
+
+ else -> {}
+ }
+ }
+ },
+ onAction = onAction,
+ )
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
+@Composable
+private fun MainAppBar(
+ modifier: Modifier = Modifier,
+ title: String,
+ subtitle: String? = null,
+ ourNode: Node?,
+ isConnected: Boolean,
+ showNodeChip: Boolean,
+ canNavigateUp: Boolean,
+ onNavigateUp: () -> Unit,
+ actions: @Composable () -> Unit,
+ onAction: (Any?) -> Unit,
+) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ },
+ subtitle = { subtitle?.let { Text(text = it) } },
+ modifier = modifier,
+ navigationIcon =
+ if (canNavigateUp) {
+ {
+ IconButton(onClick = onNavigateUp) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.navigate_back),
+ )
+ }
+ }
+ } else {
+ {
+ IconButton(enabled = false, onClick = {}) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.app_icon),
+ contentDescription = stringResource(id = R.string.application_icon),
+ )
+ }
+ }
+ },
+ actions = {
+ TopBarActions(
+ ourNode = ourNode,
+ isConnected = isConnected,
+ showNodeChip = showNodeChip,
+ actions = actions,
+ onAction = onAction,
+ )
+ },
+ )
+}
+
+@Composable
+private fun TopBarActions(
+ ourNode: Node?,
+ isConnected: Boolean,
+ showNodeChip: Boolean,
+ actions: @Composable () -> Unit,
+ onAction: (Any?) -> Unit,
+) {
+ AnimatedVisibility(showNodeChip) {
+ ourNode?.let { NodeChip(node = it, isThisNode = true, isConnected = isConnected, onAction = onAction) }
+ }
+
+ actions()
+}
+
+enum class MainMenuAction(@StringRes val stringRes: Int) {
+ DEBUG(R.string.debug_panel),
+ RADIO_CONFIG(R.string.radio_configuration),
+ EXPORT_RANGETEST(R.string.save_rangetest),
+ THEME(R.string.theme),
+ LANGUAGE(R.string.preferences_language),
+ SHOW_INTRO(R.string.intro_show),
+ QUICK_CHAT(R.string.quick_chat),
+ ABOUT(R.string.about),
+}
+
+@Composable
+private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Unit) {
+ var showMenu by remember { mutableStateOf(false) }
+ IconButton(onClick = { showMenu = true }) {
+ Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
+ }
+
+ DropdownMenu(
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ modifier = Modifier.background(colorScheme.background.copy(alpha = 1f)),
+ ) {
+ MainMenuAction.entries.forEach { action ->
+ DropdownMenuItem(
+ text = { Text(stringResource(id = action.stringRes)) },
+ onClick = {
+ onAction(action)
+ showMenu = false
+ },
+ enabled =
+ when (action) {
+ MainMenuAction.RADIO_CONFIG -> !isManaged
+ else -> true
+ },
+ )
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavigateUp: Boolean) {
+ AppTheme {
+ MainAppBar(
+ title = "Title",
+ subtitle = "Subtitle",
+ ourNode = previewNode,
+ isConnected = false,
+ showNodeChip = true,
+ canNavigateUp = canNavigateUp,
+ onNavigateUp = {},
+ actions = { MainMenuActions(isManaged = false, onAction = {}) },
+ ) {}
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/PreviewUtils.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/PreviewUtils.kt
new file mode 100644
index 000000000..dcfbfa94c
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/PreviewUtils.kt
@@ -0,0 +1,42 @@
+/*
+ * 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 .
+ */
+
+@file:Suppress("MatchingDeclarationName")
+
+package com.geeksville.mesh.ui.common.components
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.geeksville.mesh.MeshProtos
+import com.geeksville.mesh.PaxcountProtos
+import com.geeksville.mesh.TelemetryProtos
+import com.geeksville.mesh.model.Node
+
+/** Simple [PreviewParameterProvider] that provides true and false values. */
+class BooleanProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(false, true)
+}
+
+private val user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build()
+val previewNode =
+ Node(
+ num = 13444,
+ user = user,
+ isIgnored = false,
+ paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
+ environmentMetrics =
+ TelemetryProtos.EnvironmentMetrics.newBuilder().setTemperature(25f).setRelativeHumidity(60f).build(),
+ )