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(