diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 571bc53f6..3a36977bd 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -7,6 +7,7 @@
CommentSpacing:Coroutines.kt$/// Wrap launch with an exception handler, FIXME, move into a utility lib
CommentWrapping:SignalMetrics.kt$Metric.SNR$/* Selected 12 as the max to get 4 equal vertical sections. */
ComposableNaming:NodeDetailScreen.kt$notesSection
+ ComposableParamOrder:Channel.kt$ChannelScreen
ComposableParamOrder:ChannelSettingsItemList.kt$ChannelSettingsItemList
ComposableParamOrder:Debug.kt$DecodedPayloadBlock
ComposableParamOrder:DebugSearch.kt$DebugSearchState
@@ -34,6 +35,7 @@
ComposableParamOrder:PermissionScreenLayout.kt$PermissionScreenLayout
ComposableParamOrder:PowerMetrics.kt$PowerMetricsChart
ComposableParamOrder:QuickChat.kt$OutlinedTextFieldWithCounter
+ ComposableParamOrder:Share.kt$ShareScreen
ComposableParamOrder:SignalMetrics.kt$SignalMetricsChart
ComposableParamOrder:TopLevelNavIcon.kt$ConnectionsNavIcon
ComposableParamOrder:WarningDialog.kt$WarningDialog
@@ -72,6 +74,8 @@
LambdaParameterEventTrailing:MessageList.kt$onReply
LambdaParameterEventTrailing:NodeDetailScreen.kt$onClick
LambdaParameterEventTrailing:NodeDetailScreen.kt$onSaveNotes
+ LambdaParameterEventTrailing:QuickChat.kt$onNavigateUp
+ LambdaParameterEventTrailing:TracerouteLog.kt$onNavigateUp
LambdaParameterInRestartableEffect:Channel.kt$onConfirm
LambdaParameterInRestartableEffect:MessageList.kt$onUnreadChanged
LargeClass:MeshService.kt$MeshService : Service
@@ -81,6 +85,7 @@
LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
+ LongMethod:EnvironmentMetrics.kt$@Composable fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit)
LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel())
LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)
@@ -193,9 +198,11 @@
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier = modifier.width(dp)
ModifierNotUsedAtRoot:PowerMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:QuickChat.kt$modifier = modifier.fillMaxSize().padding(innerPadding)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.weight(weight = Y_AXIS_WEIGHT)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier = modifier.width(dp)
ModifierNotUsedAtRoot:SignalMetrics.kt$modifier.width(dp)
+ ModifierNotUsedAtRoot:TracerouteLog.kt$modifier = modifier.fillMaxSize().padding(innerPadding)
ModifierReused:DeviceMetrics.kt$Canvas(modifier = modifier.width(dp)) { val height = size.height val width = size.width for (i in telemetries.indices) { val telemetry = telemetries[i] /* x-value time */ val xRatio = (telemetry.time - oldest.time).toFloat() / timeDiff val x = xRatio * width /* Channel Utilization */ plotPoint( drawContext = drawContext, color = Device.CH_UTIL.color, x = x, value = telemetry.deviceMetrics.channelUtilization, divisor = MAX_PERCENT_VALUE, ) /* Air Utilization Transmit */ plotPoint( drawContext = drawContext, color = Device.AIR_UTIL.color, x = x, value = telemetry.deviceMetrics.airUtilTx, divisor = MAX_PERCENT_VALUE, ) } /* Battery Line */ var index = 0 while (index < telemetries.size) { val path = Path() index = createPath( telemetries = telemetries, index = index, path = path, oldestTime = oldest.time, timeRange = timeDiff, width = width, timeThreshold = selectedTime.timeThreshold(), ) { i -> val telemetry = telemetries.getOrNull(i) ?: telemetries.last() val ratio = telemetry.deviceMetrics.batteryLevel / MAX_PERCENT_VALUE val y = height - (ratio * height) return@createPath y } drawPath( path = path, color = Device.BATTERY.color, style = Stroke(width = GraphUtil.RADIUS, cap = StrokeCap.Round), ) } }
ModifierReused:DeviceMetrics.kt$HorizontalLinesOverlay( modifier.width(dp), lineColors = listOf(graphColor, Color.Yellow, Color.Red, graphColor, graphColor), )
ModifierReused:DeviceMetrics.kt$TimeAxisOverlay(modifier.width(dp), oldest = oldest.time, newest = newest.time, selectedTime.lineInterval())
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
index 02e741430..f1824e3d4 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
@@ -26,7 +26,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.map.NodeMapViewModel
import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle
@@ -41,9 +40,9 @@ private const val DEG_D = 1e-7
@Composable
fun NodeMapScreen(
- navController: NavHostController,
metricsViewModel: MetricsViewModel = hiltViewModel(),
nodeMapViewModel: NodeMapViewModel = hiltViewModel(),
+ onNavigateUp: () -> Unit,
) {
val density = LocalDensity.current
val state by metricsViewModel.state.collectAsStateWithLifecycle()
diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
index 81c0c778b..52a0c7447 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMapScreen.kt
@@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.navigation.NavHostController
import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.map.NodeMapViewModel
@@ -36,9 +35,9 @@ const val DEG_D = 1e-7
@Composable
fun NodeMapScreen(
- navController: NavHostController,
metricsViewModel: MetricsViewModel = hiltViewModel(),
nodeMapViewModel: NodeMapViewModel = hiltViewModel(),
+ onNavigateUp: () -> Unit,
) {
val state by metricsViewModel.state.collectAsState()
val positions = state.positionLogs
@@ -52,7 +51,7 @@ fun NodeMapScreen(
ourNode = ourNodeInfo,
showNodeChip = false,
canNavigateUp = true,
- onNavigateUp = navController::navigateUp,
+ onNavigateUp = onNavigateUp,
actions = {},
onClickChip = {},
)
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsNavigation.kt
index 04e14dbbc..e84cf926b 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ChannelsNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ChannelsNavigation.kt
@@ -40,6 +40,7 @@ fun NavGraphBuilder.channelsGraph(navController: NavHostController) {
ChannelScreen(
radioConfigViewModel = hiltViewModel(parentEntry),
onNavigate = { route -> navController.navigate(route) },
+ onNavigateUp = { navController.navigateUp() },
)
}
configRoutes(navController)
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
index 0381af0f8..9230e1a7b 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt
@@ -79,15 +79,18 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController) {
),
) { backStackEntry ->
val message = backStackEntry.toRoute().message
- ShareScreen {
- navController.navigate(ContactsRoutes.Messages(it, message)) {
- popUpTo { inclusive = true }
- }
- }
+ ShareScreen(
+ onConfirm = {
+ navController.navigate(ContactsRoutes.Messages(it, message)) {
+ popUpTo { inclusive = true }
+ }
+ },
+ onNavigateUp = navController::navigateUp,
+ )
}
composable(
deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
) {
- QuickChatScreen()
+ QuickChatScreen(onNavigateUp = navController::navigateUp)
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
index fb2c83c8f..c2064776e 100644
--- a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
+++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt
@@ -171,8 +171,7 @@ fun NavDestination.isNodeDetailRoute(): Boolean = NodeDetailRoute.entries.any {
private inline fun NavGraphBuilder.addNodeDetailScreenComposable(
navController: NavHostController,
routeInfo: NodeDetailRoute,
- crossinline screenContent:
- @Composable (navController: NavHostController, metricsViewModel: MetricsViewModel) -> Unit,
+ crossinline screenContent: @Composable (metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) -> Unit,
) {
composable(
deepLinks =
@@ -184,7 +183,7 @@ private inline fun NavGraphBuilder.addNodeDetailScreenCompos
val parentGraphBackStackEntry =
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
val metricsViewModel = hiltViewModel(parentGraphBackStackEntry)
- screenContent(navController, metricsViewModel)
+ screenContent(metricsViewModel, navController::navigateUp)
}
}
@@ -192,60 +191,60 @@ enum class NodeDetailRoute(
@StringRes val title: Int,
val route: Route,
val icon: ImageVector?,
- val screenComposable: @Composable (navController: NavHostController, metricsViewModel: MetricsViewModel) -> Unit,
+ val screenComposable: @Composable (metricsViewModel: MetricsViewModel, onNavigateUp: () -> Unit) -> Unit,
) {
DEVICE(
R.string.device,
NodeDetailRoutes.DeviceMetrics,
Icons.Default.Router,
- { _, metricsVM -> DeviceMetricsScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> DeviceMetricsScreen(metricsVM, onNavigateUp) },
),
NODE_MAP(
R.string.node_map,
NodeDetailRoutes.NodeMap,
Icons.Default.LocationOn,
- { navController, metricsVM -> NodeMapScreen(navController, metricsVM) },
+ { metricsVM, onNavigateUp -> NodeMapScreen(metricsVM, onNavigateUp = onNavigateUp) },
),
POSITION_LOG(
R.string.position_log,
NodeDetailRoutes.PositionLog,
Icons.Default.LocationOn,
- { _, metricsVM -> PositionLogScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> PositionLogScreen(metricsVM, onNavigateUp) },
),
ENVIRONMENT(
R.string.environment,
NodeDetailRoutes.EnvironmentMetrics,
Icons.Default.LightMode,
- { _, metricsVM -> EnvironmentMetricsScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> EnvironmentMetricsScreen(metricsVM, onNavigateUp) },
),
SIGNAL(
R.string.signal,
NodeDetailRoutes.SignalMetrics,
Icons.Default.CellTower,
- { _, metricsVM -> SignalMetricsScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> SignalMetricsScreen(metricsVM, onNavigateUp) },
),
TRACEROUTE(
R.string.traceroute,
NodeDetailRoutes.TracerouteLog,
Icons.Default.PermScanWifi,
- { _, metricsVM -> TracerouteLogScreen(viewModel = metricsVM) },
+ { metricsVM, onNavigateUp -> TracerouteLogScreen(viewModel = metricsVM, onNavigateUp = onNavigateUp) },
),
POWER(
R.string.power,
NodeDetailRoutes.PowerMetrics,
Icons.Default.Power,
- { _, metricsVM -> PowerMetricsScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> PowerMetricsScreen(metricsVM, onNavigateUp) },
),
HOST(
R.string.host,
NodeDetailRoutes.HostMetricsLog,
Icons.Default.Memory,
- { _, metricsVM -> HostMetricsLogScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> HostMetricsLogScreen(metricsVM, onNavigateUp) },
),
PAX(
R.string.pax,
NodeDetailRoutes.PaxMetrics,
Icons.Default.People,
- { _, metricsVM -> PaxMetricsScreen(metricsVM) },
+ { metricsVM, onNavigateUp -> PaxMetricsScreen(metricsVM, onNavigateUp) },
),
}
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 fc2ce0701..7d66a0ae3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -22,7 +22,6 @@ 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
@@ -101,13 +100,11 @@ import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.MapRoutes
-import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
-import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.icon.Conversations
@@ -338,60 +335,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
) {
Scaffold(snackbarHost = { SnackbarHost(uIViewModel.snackBarHostState) }) { _ ->
Column(modifier = Modifier.fillMaxSize()) {
- fun NavDestination.hasGlobalAppBar(): Boolean =
- // List of screens to exclude from having the global app bar
- listOf(
- ConnectionsRoutes.Connections::class,
- ContactsRoutes.Contacts::class,
- MapRoutes.Map::class,
- NodeDetailRoutes.NodeMap::class,
- NodesRoutes.Nodes::class,
- NodesRoutes.NodeDetail::class,
- SettingsRoutes.Settings::class,
- SettingsRoutes.AmbientLighting::class,
- SettingsRoutes.LoRa::class,
- SettingsRoutes.Security::class,
- SettingsRoutes.Audio::class,
- SettingsRoutes.Bluetooth::class,
- SettingsRoutes.ChannelConfig::class,
- SettingsRoutes.DetectionSensor::class,
- SettingsRoutes.Display::class,
- SettingsRoutes.Telemetry::class,
- SettingsRoutes.Network::class,
- SettingsRoutes.Paxcounter::class,
- SettingsRoutes.Power::class,
- SettingsRoutes.Position::class,
- SettingsRoutes.User::class,
- SettingsRoutes.StoreForward::class,
- SettingsRoutes.MQTT::class,
- SettingsRoutes.Serial::class,
- SettingsRoutes.ExtNotification::class,
- SettingsRoutes.CleanNodeDb::class,
- SettingsRoutes.DebugPanel::class,
- SettingsRoutes.RangeTest::class,
- SettingsRoutes.CannedMessage::class,
- SettingsRoutes.RemoteHardware::class,
- SettingsRoutes.NeighborInfo::class,
- )
- .none { this.hasRoute(it) }
-
- val ourNodeInfo by uIViewModel.ourNodeInfo.collectAsStateWithLifecycle()
- AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: false) {
- MainAppBar(
- navController = navController,
- ourNode = ourNodeInfo,
- onClickChip = {
- navController.navigate(
- NodesRoutes.NodeDetailGraph(it.num),
- {
- launchSingleTop = true
- restoreState = true
- },
- )
- },
- )
- }
-
NavHost(
navController = navController,
startDestination = NodesRoutes.NodesGraph,
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt b/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
index f1acbb64d..786dabef5 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/QuickChat.kt
@@ -48,6 +48,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -73,13 +74,18 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.dragContainer
import org.meshtastic.core.ui.component.dragDropItemsIndexed
import org.meshtastic.core.ui.component.rememberDragDropState
import org.meshtastic.core.ui.theme.AppTheme
@Composable
-internal fun QuickChatScreen(modifier: Modifier = Modifier, viewModel: UIViewModel = hiltViewModel()) {
+internal fun QuickChatScreen(
+ modifier: Modifier = Modifier,
+ viewModel: UIViewModel = hiltViewModel(),
+ onNavigateUp: () -> Unit,
+) {
val actions by viewModel.quickChatActions.collectAsStateWithLifecycle()
var showActionDialog by remember { mutableStateOf(null) }
@@ -90,38 +96,51 @@ internal fun QuickChatScreen(modifier: Modifier = Modifier, viewModel: UIViewMod
viewModel.updateActionPositions(list)
}
- Box(modifier = modifier.fillMaxSize()) {
- if (showActionDialog != null) {
- val action = showActionDialog ?: return
- EditQuickChatDialog(
- action = action,
- onSave = viewModel::addQuickChatAction,
- onDelete = viewModel::deleteQuickChatAction,
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = stringResource(id = R.string.quick_chat),
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
+ )
+ },
+ ) { innerPadding ->
+ Box(modifier = modifier.fillMaxSize().padding(innerPadding)) {
+ showActionDialog?.let {
+ EditQuickChatDialog(
+ action = it,
+ onSave = viewModel::addQuickChatAction,
+ onDelete = viewModel::deleteQuickChatAction,
+ ) {
+ showActionDialog = null
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
+ state = listState,
+ contentPadding = PaddingValues(16.dp),
) {
- showActionDialog = null
+ dragDropItemsIndexed(items = actions, dragDropState = dragDropState, key = { _, item -> item.uuid }) {
+ _,
+ action,
+ isDragging,
+ ->
+ QuickChatItem(action = action, onEdit = { showActionDialog = it })
+ }
}
- }
- LazyColumn(
- modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
- state = listState,
- contentPadding = PaddingValues(16.dp),
- ) {
- dragDropItemsIndexed(items = actions, dragDropState = dragDropState, key = { _, item -> item.uuid }) {
- _,
- action,
- isDragging,
- ->
- QuickChatItem(action = action, onEdit = { showActionDialog = it })
+ FloatingActionButton(
+ onClick = { showActionDialog = QuickChatAction(position = actions.size) },
+ modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
+ ) {
+ Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(id = R.string.add))
}
}
-
- FloatingActionButton(
- onClick = { showActionDialog = QuickChatAction(position = actions.size) },
- modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
- ) {
- Icon(imageVector = Icons.Default.Add, contentDescription = stringResource(id = R.string.add))
- }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
index f449db185..3bd7467a3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/DeviceMetrics.kt
@@ -36,6 +36,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -70,6 +71,7 @@ import com.geeksville.mesh.util.GraphUtil
import com.geeksville.mesh.util.GraphUtil.createPath
import com.geeksville.mesh.util.GraphUtil.plotPoint
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MaterialBatteryInfo
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
@@ -114,41 +116,55 @@ private val LEGEND_DATA =
)
@Composable
-fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
+fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
var displayInfoDialog by remember { mutableStateOf(false) }
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
val data = state.deviceMetricsFiltered(selectedTimeFrame)
- Column {
- if (displayInfoDialog) {
- LegendInfoDialog(
- pairedRes =
- listOf(
- Pair(R.string.channel_utilization, R.string.ch_util_definition),
- Pair(R.string.air_utilization, R.string.air_util_definition),
- ),
- onDismiss = { displayInfoDialog = false },
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ if (displayInfoDialog) {
+ LegendInfoDialog(
+ pairedRes =
+ listOf(
+ Pair(R.string.channel_utilization, R.string.ch_util_definition),
+ Pair(R.string.air_utilization, R.string.air_util_definition),
+ ),
+ onDismiss = { displayInfoDialog = false },
+ )
+ }
+
+ DeviceMetricsChart(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
+ telemetries = data.reversed(),
+ selectedTimeFrame,
+ promptInfoDialog = { displayInfoDialog = true },
+ )
+
+ SlidingSelector(
+ TimeFrame.entries.toList(),
+ selectedTimeFrame,
+ onOptionSelected = { viewModel.setTimeFrame(it) },
+ ) {
+ OptionLabel(stringResource(it.strRes))
+ }
+
+ /* Device Metric Cards */
+ LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> DeviceMetricsCard(telemetry) } }
}
-
- DeviceMetricsChart(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
- telemetries = data.reversed(),
- selectedTimeFrame,
- promptInfoDialog = { displayInfoDialog = true },
- )
-
- SlidingSelector(
- TimeFrame.entries.toList(),
- selectedTimeFrame,
- onOptionSelected = { viewModel.setTimeFrame(it) },
- ) {
- OptionLabel(stringResource(it.strRes))
- }
-
- /* Device Metric Cards */
- LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> DeviceMetricsCard(telemetry) } }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
index 573bd9f74..152a2acdf 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -59,11 +60,12 @@ import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.IaqDisplayMode
import org.meshtastic.core.ui.component.IndoorAirQuality
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
@Composable
-fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
+fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val environmentState by viewModel.environmentState.collectAsStateWithLifecycle()
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
@@ -88,32 +90,47 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
}
var displayInfoDialog by remember { mutableStateOf(false) }
- Column {
- if (displayInfoDialog) {
- LegendInfoDialog(
- pairedRes = listOf(Pair(R.string.iaq, R.string.iaq_definition)),
- onDismiss = { displayInfoDialog = false },
+
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
- }
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ if (displayInfoDialog) {
+ LegendInfoDialog(
+ pairedRes = listOf(Pair(R.string.iaq, R.string.iaq_definition)),
+ onDismiss = { displayInfoDialog = false },
+ )
+ }
- EnvironmentMetricsChart(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
- telemetries = processedTelemetries.reversed(),
- graphData = graphData,
- selectedTime = selectedTimeFrame,
- promptInfoDialog = { displayInfoDialog = true },
- )
+ EnvironmentMetricsChart(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
+ telemetries = processedTelemetries.reversed(),
+ graphData = graphData,
+ selectedTime = selectedTimeFrame,
+ promptInfoDialog = { displayInfoDialog = true },
+ )
- SlidingSelector(
- TimeFrame.entries.toList(),
- selectedTimeFrame,
- onOptionSelected = { viewModel.setTimeFrame(it) },
- ) {
- OptionLabel(stringResource(it.strRes))
- }
+ SlidingSelector(
+ TimeFrame.entries.toList(),
+ selectedTimeFrame,
+ onOptionSelected = { viewModel.setTimeFrame(it) },
+ ) {
+ OptionLabel(stringResource(it.strRes))
+ }
- LazyColumn(modifier = Modifier.fillMaxSize()) {
- items(processedTelemetries) { telemetry -> EnvironmentMetricsCard(telemetry, state.isFahrenheit) }
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(processedTelemetries) { telemetry -> EnvironmentMetricsCard(telemetry, state.isFahrenheit) }
+ }
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
index 93d5eaf3b..01ae70323 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/HostMetricsLog.kt
@@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -58,18 +59,36 @@ import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DecimalFormat
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel()) {
+fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by metricsViewModel.state.collectAsStateWithLifecycle()
val hostMetrics = state.hostMetrics
- LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp)) {
- items(hostMetrics) { telemetry -> HostMetricsItem(telemetry = telemetry) }
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
+ )
+ },
+ ) { innerPadding ->
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().padding(innerPadding),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ ) {
+ items(hostMetrics) { telemetry -> HostMetricsItem(telemetry = telemetry) }
+ }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
index 3c67e930b..c3f13d430 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PaxMetrics.kt
@@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -60,6 +61,7 @@ import com.geeksville.mesh.model.TimeFrame
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import java.text.DateFormat
@@ -153,7 +155,7 @@ private fun PaxMetricsChart(
@Composable
@Suppress("MagicNumber", "LongMethod")
-fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel()) {
+fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by metricsViewModel.state.collectAsStateWithLifecycle()
val dateFormat = DateFormat.getDateTimeInstance()
var timeFrame by remember { mutableStateOf(TimeFrame.TWENTY_FOUR_HOURS) }
@@ -189,38 +191,52 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel()) {
LegendData(PaxSeries.WIFI.legendRes, PaxSeries.WIFI.color, environmentMetric = null),
)
- Column(modifier = Modifier.fillMaxSize()) {
- // Time frame selector
- SlidingSelector(
- options = TimeFrame.entries.toList(),
- selectedOption = timeFrame,
- onOptionSelected = { timeFrame = it },
- ) { tf: TimeFrame ->
- OptionLabel(stringResource(tf.strRes))
- }
- // Graph
- if (graphData.isNotEmpty()) {
- ChartHeader(graphData.size)
- Legend(legendData = legendData)
- PaxMetricsChart(
- totalSeries = totalSeries,
- bleSeries = bleSeries,
- wifiSeries = wifiSeries,
- minValue = minValue,
- maxValue = maxValue,
- timeFrame = timeFrame,
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
- }
- // List
- if (paxMetrics.isEmpty()) {
- Text(
- text = stringResource(R.string.no_pax_metrics_logs),
- modifier = Modifier.fillMaxSize().padding(16.dp),
- textAlign = TextAlign.Center,
- )
- } else {
- LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp)) {
- items(paxMetrics) { (log, pax) -> PaxMetricsItem(log, pax, dateFormat) }
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
+ // Time frame selector
+ SlidingSelector(
+ options = TimeFrame.entries.toList(),
+ selectedOption = timeFrame,
+ onOptionSelected = { timeFrame = it },
+ ) { tf: TimeFrame ->
+ OptionLabel(stringResource(tf.strRes))
+ }
+ // Graph
+ if (graphData.isNotEmpty()) {
+ ChartHeader(graphData.size)
+ Legend(legendData = legendData)
+ PaxMetricsChart(
+ totalSeries = totalSeries,
+ bleSeries = bleSeries,
+ wifiSeries = wifiSeries,
+ minValue = minValue,
+ maxValue = maxValue,
+ timeFrame = timeFrame,
+ )
+ }
+ // List
+ if (paxMetrics.isEmpty()) {
+ Text(
+ text = stringResource(R.string.no_pax_metrics_logs),
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ textAlign = TextAlign.Center,
+ )
+ } else {
+ LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp)) {
+ items(paxMetrics) { (log, pax) -> PaxMetricsItem(log, pax, dateFormat) }
+ }
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
index 4fad6c5f0..ca08831c5 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PositionLog.kt
@@ -43,6 +43,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -67,6 +68,7 @@ import com.geeksville.mesh.model.MetricsViewModel
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.theme.AppTheme
import java.text.DateFormat
import kotlin.time.Duration.Companion.days
@@ -171,7 +173,7 @@ private fun ActionButtons(
}
@Composable
-fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel()) {
+fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val exportPositionLauncher =
@@ -183,37 +185,51 @@ fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel()) {
var clearButtonEnabled by rememberSaveable(state.positionLogs) { mutableStateOf(state.positionLogs.isNotEmpty()) }
- BoxWithConstraints {
- val compactWidth = maxWidth < 600.dp
- Column {
- val textStyle =
- if (compactWidth) {
- MaterialTheme.typography.bodySmall
- } else {
- LocalTextStyle.current
- }
- CompositionLocalProvider(LocalTextStyle provides textStyle) {
- HeaderItem(compactWidth)
- PositionList(compactWidth, state.positionLogs, state.displayUnits)
- }
-
- ActionButtons(
- clearButtonEnabled = clearButtonEnabled,
- onClear = {
- clearButtonEnabled = false
- viewModel.clearPosition()
- },
- saveButtonEnabled = state.hasPositionLogs(),
- onSave = {
- val intent =
- Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "application/*"
- putExtra(Intent.EXTRA_TITLE, "position.csv")
- }
- exportPositionLauncher.launch(intent)
- },
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
+ },
+ ) { innerPadding ->
+ BoxWithConstraints(modifier = Modifier.padding(innerPadding)) {
+ val compactWidth = maxWidth < 600.dp
+ Column {
+ val textStyle =
+ if (compactWidth) {
+ MaterialTheme.typography.bodySmall
+ } else {
+ LocalTextStyle.current
+ }
+ CompositionLocalProvider(LocalTextStyle provides textStyle) {
+ HeaderItem(compactWidth)
+ PositionList(compactWidth, state.positionLogs, state.displayUnits)
+ }
+
+ ActionButtons(
+ clearButtonEnabled = clearButtonEnabled,
+ onClear = {
+ clearButtonEnabled = false
+ viewModel.clearPosition()
+ },
+ saveButtonEnabled = state.hasPositionLogs(),
+ onSave = {
+ val intent =
+ Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/*"
+ putExtra(Intent.EXTRA_TITLE, "position.csv")
+ }
+ exportPositionLauncher.launch(intent)
+ },
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
index 5c81d3f7e..e1d71578d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/PowerMetrics.kt
@@ -37,6 +37,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -67,6 +68,7 @@ import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.util.GraphUtil
import com.geeksville.mesh.util.GraphUtil.createPath
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
@@ -117,33 +119,50 @@ private val LEGEND_DATA =
)
@Composable
-fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
+fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
var selectedChannel by remember { mutableStateOf(PowerChannel.ONE) }
val data = state.powerMetricsFiltered(selectedTimeFrame)
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
+ )
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ PowerMetricsChart(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
+ telemetries = data.reversed(),
+ selectedTimeFrame,
+ selectedChannel,
+ )
- Column {
- PowerMetricsChart(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
- telemetries = data.reversed(),
- selectedTimeFrame,
- selectedChannel,
- )
+ SlidingSelector(
+ PowerChannel.entries.toList(),
+ selectedChannel,
+ onOptionSelected = { selectedChannel = it },
+ ) {
+ OptionLabel(stringResource(it.strRes))
+ }
+ Spacer(modifier = Modifier.height(2.dp))
+ SlidingSelector(
+ TimeFrame.entries.toList(),
+ selectedTimeFrame,
+ onOptionSelected = { viewModel.setTimeFrame(it) },
+ ) {
+ OptionLabel(stringResource(it.strRes))
+ }
- SlidingSelector(PowerChannel.entries.toList(), selectedChannel, onOptionSelected = { selectedChannel = it }) {
- OptionLabel(stringResource(it.strRes))
+ LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> PowerMetricsCard(telemetry) } }
}
- Spacer(modifier = Modifier.height(2.dp))
- SlidingSelector(
- TimeFrame.entries.toList(),
- selectedTimeFrame,
- onOptionSelected = { viewModel.setTimeFrame(it) },
- ) {
- OptionLabel(stringResource(it.strRes))
- }
-
- LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { telemetry -> PowerMetricsCard(telemetry) } }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
index fb1f06128..46d4cb1c5 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/SignalMetrics.kt
@@ -37,6 +37,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -64,6 +65,7 @@ import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import com.geeksville.mesh.util.GraphUtil.plotPoint
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.LoraSignalIndicator
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.OptionLabel
import org.meshtastic.core.ui.component.SlidingSelector
import org.meshtastic.core.ui.component.SnrAndRssi
@@ -89,37 +91,56 @@ private val LEGEND_DATA =
)
@Composable
-fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel()) {
+fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
var displayInfoDialog by remember { mutableStateOf(false) }
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
val data = state.signalMetricsFiltered(selectedTimeFrame)
- Column {
- if (displayInfoDialog) {
- LegendInfoDialog(
- pairedRes =
- listOf(Pair(R.string.snr, R.string.snr_definition), Pair(R.string.rssi, R.string.rssi_definition)),
- onDismiss = { displayInfoDialog = false },
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ if (displayInfoDialog) {
+ LegendInfoDialog(
+ pairedRes =
+ listOf(
+ Pair(R.string.snr, R.string.snr_definition),
+ Pair(R.string.rssi, R.string.rssi_definition),
+ ),
+ onDismiss = { displayInfoDialog = false },
+ )
+ }
+
+ SignalMetricsChart(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
+ meshPackets = data.reversed(),
+ selectedTimeFrame,
+ promptInfoDialog = { displayInfoDialog = true },
+ )
+
+ SlidingSelector(
+ TimeFrame.entries.toList(),
+ selectedTimeFrame,
+ onOptionSelected = { viewModel.setTimeFrame(it) },
+ ) {
+ OptionLabel(stringResource(it.strRes))
+ }
+
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(data) { meshPacket -> SignalMetricsCard(meshPacket) }
+ }
}
-
- SignalMetricsChart(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(fraction = 0.33f),
- meshPackets = data.reversed(),
- selectedTimeFrame,
- promptInfoDialog = { displayInfoDialog = true },
- )
-
- SlidingSelector(
- TimeFrame.entries.toList(),
- selectedTimeFrame,
- onOptionSelected = { viewModel.setTimeFrame(it) },
- ) {
- OptionLabel(stringResource(it.strRes))
- }
-
- LazyColumn(modifier = Modifier.fillMaxSize()) { items(data) { meshPacket -> SignalMetricsCard(meshPacket) } }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
index 41dafc679..49ca62a17 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
@@ -41,6 +41,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -67,6 +68,7 @@ import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD
import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD
import org.meshtastic.core.ui.component.SimpleAlertDialog
@@ -79,7 +81,11 @@ import java.text.DateFormat
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod")
@Composable
-fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewModel = hiltViewModel()) {
+fun TracerouteLogScreen(
+ modifier: Modifier = Modifier,
+ viewModel: MetricsViewModel = hiltViewModel(),
+ onNavigateUp: () -> Unit,
+) {
val state by viewModel.state.collectAsStateWithLifecycle()
val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) }
@@ -96,55 +102,75 @@ fun TracerouteLogScreen(modifier: Modifier = Modifier, viewModel: MetricsViewMod
)
}
- LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp)) {
- items(state.tracerouteRequests, key = { it.uuid }) { log ->
- val result =
- remember(state.tracerouteRequests, log.fromRadio.packet.id) {
- state.tracerouteResults.find { it.fromRadio.packet.decoded.requestId == log.fromRadio.packet.id }
- }
- val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery }
-
- val time = dateFormat.format(log.received_date)
- val (text, icon) = route.getTextAndIcon()
- var expanded by remember { mutableStateOf(false) }
-
- val tracerouteDetailsAnnotated: AnnotatedString? =
- result?.let { res ->
- if (route != null && route.routeList.isNotEmpty() && route.routeBackList.isNotEmpty()) {
- val seconds = (res.received_date - log.received_date).coerceAtLeast(0).toDouble() / MS_PER_SEC
- val annotatedBase =
- annotateTraceroute(res.fromRadio.packet.getTracerouteResponse(::getUsername))
- buildAnnotatedString {
- append(annotatedBase)
- append("\n\nDuration: ${"%.1f".format(seconds)} s")
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = state.node?.user?.longName ?: "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
+ )
+ },
+ ) { innerPadding ->
+ LazyColumn(
+ modifier = modifier.fillMaxSize().padding(innerPadding),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ ) {
+ items(state.tracerouteRequests, key = { it.uuid }) { log ->
+ val result =
+ remember(state.tracerouteRequests, log.fromRadio.packet.id) {
+ state.tracerouteResults.find {
+ it.fromRadio.packet.decoded.requestId == log.fromRadio.packet.id
}
- } else {
- // For cases where there's a result but no full route, display plain text
- res.fromRadio.packet.getTracerouteResponse(::getUsername)?.let { AnnotatedString(it) }
}
- }
+ val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery }
- Box {
- TracerouteItem(
- icon = icon,
- text = "$time - $text",
- modifier =
- Modifier.combinedClickable(onLongClick = { expanded = true }) {
- if (tracerouteDetailsAnnotated != null) {
- showDialog = tracerouteDetailsAnnotated
- } else if (result != null) {
- // Fallback for results that couldn't be fully annotated but have basic info
- val basicInfo = result.fromRadio.packet.getTracerouteResponse(::getUsername)
- if (basicInfo != null) {
- showDialog = AnnotatedString(basicInfo)
+ val time = dateFormat.format(log.received_date)
+ val (text, icon) = route.getTextAndIcon()
+ var expanded by remember { mutableStateOf(false) }
+
+ val tracerouteDetailsAnnotated: AnnotatedString? =
+ result?.let { res ->
+ if (route != null && route.routeList.isNotEmpty() && route.routeBackList.isNotEmpty()) {
+ val seconds =
+ (res.received_date - log.received_date).coerceAtLeast(0).toDouble() / MS_PER_SEC
+ val annotatedBase =
+ annotateTraceroute(res.fromRadio.packet.getTracerouteResponse(::getUsername))
+ buildAnnotatedString {
+ append(annotatedBase)
+ append("\n\nDuration: ${"%.1f".format(seconds)} s")
}
+ } else {
+ // For cases where there's a result but no full route, display plain text
+ res.fromRadio.packet.getTracerouteResponse(::getUsername)?.let { AnnotatedString(it) }
+ }
+ }
+
+ Box {
+ TracerouteItem(
+ icon = icon,
+ text = "$time - $text",
+ modifier =
+ Modifier.combinedClickable(onLongClick = { expanded = true }) {
+ if (tracerouteDetailsAnnotated != null) {
+ showDialog = tracerouteDetailsAnnotated
+ } else if (result != null) {
+ // Fallback for results that couldn't be fully annotated but have basic info
+ val basicInfo = result.fromRadio.packet.getTracerouteResponse(::getUsername)
+ if (basicInfo != null) {
+ showDialog = AnnotatedString(basicInfo)
+ }
+ }
+ },
+ )
+ DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ DeleteItem {
+ viewModel.deleteLog(log.uuid)
+ expanded = false
}
- },
- )
- DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
- DeleteItem {
- viewModel.deleteLog(log.uuid)
- expanded = false
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index b7e2d0975..cce17689e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -51,6 +51,7 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
@@ -115,6 +116,7 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.PreferenceFooter
import timber.log.Timber
@@ -129,6 +131,7 @@ fun ChannelScreen(
viewModel: ChannelViewModel = hiltViewModel(),
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
onNavigate: (Route) -> Unit,
+ onNavigateUp: () -> Unit,
) {
val focusManager = LocalFocusManager.current
@@ -274,73 +277,91 @@ fun ChannelScreen(
requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) }
- val listState = rememberLazyListState()
- LazyColumn(state = listState, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp)) {
- item {
- ChannelListView(
- enabled = enabled,
- channelSet = channelSet,
- modemPresetName = modemPresetName,
- channelSelections = channelSelections,
- shouldAddChannel = shouldAddChannelsState,
- onClick = {
- isWaiting = true
- radioConfigViewModel.setResponseStateLoading(ConfigRoute.CHANNELS)
- },
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = "",
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
- EditChannelUrl(
- enabled = enabled,
- channelUrl = selectedChannelSet.getChannelUrl(shouldAdd = shouldAddChannelsState),
- onConfirm = {
- viewModel.requestChannelUrl(it) {
- Toast.makeText(context, R.string.channel_invalid, Toast.LENGTH_SHORT).show()
- }
- },
- )
- }
- item {
- SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
- SegmentedButton(
- label = { Text(text = stringResource(R.string.replace)) },
- onClick = { shouldAddChannelsState = false },
- selected = !shouldAddChannelsState,
- shape = SegmentedButtonDefaults.itemShape(0, 2),
+ },
+ ) { innerPadding ->
+ val listState = rememberLazyListState()
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.padding(innerPadding),
+ contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
+ ) {
+ item {
+ ChannelListView(
+ enabled = enabled,
+ channelSet = channelSet,
+ modemPresetName = modemPresetName,
+ channelSelections = channelSelections,
+ shouldAddChannel = shouldAddChannelsState,
+ onClick = {
+ isWaiting = true
+ radioConfigViewModel.setResponseStateLoading(ConfigRoute.CHANNELS)
+ },
)
- SegmentedButton(
- label = { Text(text = stringResource(R.string.add)) },
- onClick = { shouldAddChannelsState = true },
- selected = shouldAddChannelsState,
- shape = SegmentedButtonDefaults.itemShape(1, 2),
+ EditChannelUrl(
+ enabled = enabled,
+ channelUrl = selectedChannelSet.getChannelUrl(shouldAdd = shouldAddChannelsState),
+ onConfirm = {
+ viewModel.requestChannelUrl(it) {
+ Toast.makeText(context, R.string.channel_invalid, Toast.LENGTH_SHORT).show()
+ }
+ },
+ )
+ }
+ item {
+ SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
+ SegmentedButton(
+ label = { Text(text = stringResource(R.string.replace)) },
+ onClick = { shouldAddChannelsState = false },
+ selected = !shouldAddChannelsState,
+ shape = SegmentedButtonDefaults.itemShape(0, 2),
+ )
+ SegmentedButton(
+ label = { Text(text = stringResource(R.string.add)) },
+ onClick = { shouldAddChannelsState = true },
+ selected = shouldAddChannelsState,
+ shape = SegmentedButtonDefaults.itemShape(1, 2),
+ )
+ }
+ }
+ item {
+ ModemPresetInfo(
+ modemPresetName = modemPresetName,
+ onClick = {
+ isWaiting = true
+ radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
+ },
+ )
+ }
+ item {
+ PreferenceFooter(
+ enabled = enabled,
+ negativeText = R.string.reset,
+ onNegativeClicked = {
+ focusManager.clearFocus()
+ showResetDialog = true
+ },
+ positiveText = R.string.scan,
+ onPositiveClicked = {
+ focusManager.clearFocus()
+ if (cameraPermissionState.status.isGranted) {
+ zxingScan()
+ } else {
+ cameraPermissionState.launchPermissionRequest()
+ }
+ },
)
}
- }
- item {
- ModemPresetInfo(
- modemPresetName = modemPresetName,
- onClick = {
- isWaiting = true
- radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
- },
- )
- }
- item {
- PreferenceFooter(
- enabled = enabled,
- negativeText = R.string.reset,
- onNegativeClicked = {
- focusManager.clearFocus()
- showResetDialog = true
- },
- positiveText = R.string.scan,
- onPositiveClicked = {
- focusManager.clearFocus()
- if (cameraPermissionState.status.isGranted) {
- zxingScan()
- } else {
- cameraPermissionState.launchPermissionRequest()
- }
- },
- )
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
index 4bb59582d..6ba47fd88 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Share.kt
@@ -27,6 +27,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -43,40 +44,59 @@ import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.ui.contact.ContactItem
import com.geeksville.mesh.ui.contact.ContactsViewModel
import org.meshtastic.core.strings.R
+import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.theme.AppTheme
@Composable
-fun ShareScreen(viewModel: ContactsViewModel = hiltViewModel(), onConfirm: (String) -> Unit) {
+fun ShareScreen(viewModel: ContactsViewModel = hiltViewModel(), onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) {
val contactList by viewModel.contactList.collectAsStateWithLifecycle()
- ShareScreen(contacts = contactList, onConfirm = onConfirm)
+ ShareScreen(contacts = contactList, onConfirm = onConfirm, onNavigateUp = onNavigateUp)
}
@Composable
-fun ShareScreen(contacts: List, onConfirm: (String) -> Unit) {
+fun ShareScreen(contacts: List, onConfirm: (String) -> Unit, onNavigateUp: () -> Unit) {
var selectedContact by remember { mutableStateOf("") }
- Column {
- LazyColumn(
- modifier = Modifier.weight(1f),
- contentPadding = PaddingValues(6.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- items(contacts, key = { it.contactKey }) { contact ->
- val selected = contact.contactKey == selectedContact
- ContactItem(contact = contact, selected = selected, onClick = { selectedContact = contact.contactKey })
- }
- }
-
- Button(
- onClick = { onConfirm(selectedContact) },
- modifier = Modifier.fillMaxWidth().padding(24.dp),
- enabled = selectedContact.isNotEmpty(),
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Default.Send,
- contentDescription = stringResource(id = R.string.share),
+ Scaffold(
+ topBar = {
+ MainAppBar(
+ title = stringResource(id = R.string.share_to),
+ ourNode = null,
+ showNodeChip = false,
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ actions = {},
+ onClickChip = {},
)
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ contentPadding = PaddingValues(6.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(contacts, key = { it.contactKey }) { contact ->
+ val selected = contact.contactKey == selectedContact
+ ContactItem(
+ contact = contact,
+ selected = selected,
+ onClick = { selectedContact = contact.contactKey },
+ )
+ }
+ }
+
+ Button(
+ onClick = { onConfirm(selectedContact) },
+ modifier = Modifier.fillMaxWidth().padding(24.dp),
+ enabled = selectedContact.isNotEmpty(),
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.Send,
+ contentDescription = stringResource(id = R.string.share),
+ )
+ }
}
}
}
@@ -101,6 +121,7 @@ private fun ShareScreenPreview() {
),
),
onConfirm = {},
+ onNavigateUp = {},
)
}
}
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
index 901996bd1..b13cf61f3 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/MainAppBar.kt
@@ -31,7 +31,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@@ -40,57 +39,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
-import androidx.navigation.NavDestination.Companion.hasRoute
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.currentBackStackEntryAsState
import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.navigation.ContactsRoutes
-import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.preview.BooleanProvider
import org.meshtastic.core.ui.component.preview.previewNode
import org.meshtastic.core.ui.theme.AppTheme
-@Suppress("CyclomaticComplexMethod")
-@Composable
-fun MainAppBar(
- modifier: Modifier = Modifier,
- navController: NavHostController,
- ourNode: Node?,
- onClickChip: (Node) -> Unit,
-) {
- val backStackEntry by navController.currentBackStackEntryAsState()
- val currentDestination = backStackEntry?.destination
- if (currentDestination?.hasRoute() == true) {
- return
- }
-
- val title: String =
- when {
- currentDestination == null -> ""
-
- currentDestination.hasRoute() -> stringResource(id = R.string.debug_panel)
-
- currentDestination.hasRoute() -> stringResource(id = R.string.quick_chat)
-
- currentDestination.hasRoute() -> stringResource(id = R.string.share_to)
-
- else -> ""
- }
-
- MainAppBar(
- modifier = modifier,
- title = title,
- subtitle = null,
- canNavigateUp = navController.previousBackStackEntry != null,
- ourNode = ourNode,
- showNodeChip = false,
- onNavigateUp = navController::navigateUp,
- actions = {},
- onClickChip = onClickChip,
- )
-}
-
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MainAppBar(