From c9d39b22e7d69a22a4dc7a271207d9c7151d1c2d Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:50:12 -0400 Subject: [PATCH 01/10] Remove SettingsScreen global app bar --- .../main/java/com/geeksville/mesh/ui/Main.kt | 47 +-- .../mesh/ui/common/components/MainAppBar.kt | 2 +- .../mesh/ui/settings/SettingsScreen.kt | 293 +++++++++--------- 3 files changed, 183 insertions(+), 159 deletions(-) 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 fd09930513..ea2a5ed36e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -22,6 +22,7 @@ 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 @@ -135,7 +136,6 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, NodesRoutes.Nodes::class, MapRoutes.Map::class, ConnectionsRoutes.Connections::class, - SettingsRoutes.Settings::class, ) .any { this.hasRoute(it) } @@ -349,26 +349,33 @@ fun MainScreen( if (sharedContact != null) { SharedContactDialog(contact = sharedContact, onDismiss = { sharedContact = null }) } - MainAppBar( - viewModel = uIViewModel, - navController = navController, - onAction = { action -> - when (action) { - is NodeMenuAction.MoreDetails -> { - navController.navigate( - NodesRoutes.NodeDetailGraph(action.node.num), - { - launchSingleTop = true - restoreState = true - }, - ) - } - is NodeMenuAction.Share -> sharedContact = action.node - else -> {} - } - }, - ) + fun NavDestination.hasGlobalAppBar(): Boolean = + // List of screens to exclude from having the global app bar + listOf>(SettingsRoutes.Settings::class).none { this.hasRoute(it) } + + AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) { + MainAppBar( + viewModel = uIViewModel, + navController = navController, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> { + navController.navigate( + NodesRoutes.NodeDetailGraph(action.node.num), + { + launchSingleTop = true + restoreState = true + }, + ) + } + + is NodeMenuAction.Share -> sharedContact = action.node + else -> {} + } + }, + ) + } NavHost( navController = navController, diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt index 8a2d9454e9..99f4543586 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt @@ -125,7 +125,7 @@ fun MainAppBar( @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable -private fun MainAppBar( +fun MainAppBar( modifier: Modifier = Modifier, title: String, subtitle: String? = null, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index 12728639e9..aa66bb40ef 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.rounded.LocationOn import androidx.compose.material.icons.rounded.Memory import androidx.compose.material.icons.rounded.Output import androidx.compose.material.icons.rounded.WavingHand +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -59,6 +60,7 @@ import com.geeksville.mesh.android.gpsDisabled import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.getNavRouteFrom +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.common.components.TitledCard import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC import com.geeksville.mesh.ui.settings.components.SettingsItem @@ -85,10 +87,10 @@ fun SettingsScreen( uiViewModel: UIViewModel = hiltViewModel(), onNavigate: (Route) -> Unit = {}, ) { - uiViewModel.setTitle(stringResource(R.string.bottom_nav_settings)) - val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle() + val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false) val state by viewModel.radioConfigState.collectAsStateWithLifecycle() var isWaiting by remember { mutableStateOf(false) } @@ -162,163 +164,178 @@ fun SettingsScreen( ) } - Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(16.dp)) { - RadioConfigItemList( - state = state, - isManaged = localConfig.security.isManaged, - excludedModulesUnlocked = excludedModulesUnlocked, - onRouteClick = { route -> - isWaiting = true - viewModel.setResponseStateLoading(route) - }, - onImport = { - viewModel.clearPacketResponse() - deviceProfile = null - val intent = - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - } - importConfigLauncher.launch(intent) - }, - onExport = { - viewModel.clearPacketResponse() - deviceProfile = null - showEditDeviceProfileDialog = true - }, - onNavigate = onNavigate, - ) + Scaffold( + topBar = { + MainAppBar( + title = stringResource(R.string.bottom_nav_settings), + ourNode = ourNode, + isConnected = isConnected, + showNodeChip = true, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = {}, + ) + }, + ) { paddingValues -> + Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp)) { + RadioConfigItemList( + state = state, + isManaged = localConfig.security.isManaged, + excludedModulesUnlocked = excludedModulesUnlocked, + onRouteClick = { route -> + isWaiting = true + viewModel.setResponseStateLoading(route) + }, + onImport = { + viewModel.clearPacketResponse() + deviceProfile = null + val intent = + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/*" + } + importConfigLauncher.launch(intent) + }, + onExport = { + viewModel.clearPacketResponse() + deviceProfile = null + showEditDeviceProfileDialog = true + }, + onNavigate = onNavigate, + ) - val context = LocalContext.current + val context = LocalContext.current - TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) { - if (state.analyticsAvailable) { - SettingsItemSwitch( - text = stringResource(R.string.analytics_okay), - checked = state.analyticsEnabled, - leadingIcon = Icons.Default.BugReport, - onClick = { viewModel.toggleAnalytics() }, - ) - } + TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) { + if (state.analyticsAvailable) { + SettingsItemSwitch( + text = stringResource(R.string.analytics_okay), + checked = state.analyticsEnabled, + leadingIcon = Icons.Default.BugReport, + onClick = { viewModel.toggleAnalytics() }, + ) + } - val locationPermissionsState = - rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION)) - val isGpsDisabled = context.gpsDisabled() - val provideLocation by uiViewModel.provideLocation.collectAsState(false) + val locationPermissionsState = + rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION)) + val isGpsDisabled = context.gpsDisabled() + val provideLocation by uiViewModel.provideLocation.collectAsState(false) - LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) { - if (provideLocation) { - if (locationPermissionsState.allPermissionsGranted) { - if (!isGpsDisabled) { - uiViewModel.meshService?.startProvideLocation() + LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) { + if (provideLocation) { + if (locationPermissionsState.allPermissionsGranted) { + if (!isGpsDisabled) { + uiViewModel.meshService?.startProvideLocation() + } else { + uiViewModel.showSnackBar(context.getString(R.string.location_disabled)) + } } else { - uiViewModel.showSnackBar(context.getString(R.string.location_disabled)) + // Request permissions if not granted and user wants to provide location + locationPermissionsState.launchMultiplePermissionRequest() } } else { - // Request permissions if not granted and user wants to provide location - locationPermissionsState.launchMultiplePermissionRequest() + uiViewModel.meshService?.stopProvideLocation() } - } else { - uiViewModel.meshService?.stopProvideLocation() } - } - SettingsItemSwitch( - text = stringResource(R.string.provide_location_to_mesh), - leadingIcon = Icons.Rounded.LocationOn, - enabled = !isGpsDisabled, - checked = provideLocation, - ) { - uiViewModel.setProvideLocation(!provideLocation) - } + SettingsItemSwitch( + text = stringResource(R.string.provide_location_to_mesh), + leadingIcon = Icons.Rounded.LocationOn, + enabled = !isGpsDisabled, + checked = provideLocation, + ) { + uiViewModel.setProvideLocation(!provideLocation) + } - val languageTags = remember { LanguageUtils.getLanguageTags(context) } - SettingsItem( - text = stringResource(R.string.preferences_language), - leadingIcon = Icons.Rounded.Language, - trailingIcon = null, - ) { - val lang = LanguageUtils.getLocale() - debug("Lang from prefs: $lang") - val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } } + val languageTags = remember { LanguageUtils.getLanguageTags(context) } + SettingsItem( + text = stringResource(R.string.preferences_language), + leadingIcon = Icons.Rounded.Language, + trailingIcon = null, + ) { + val lang = LanguageUtils.getLocale() + debug("Lang from prefs: $lang") + val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } } - uiViewModel.showAlert( - title = context.getString(R.string.preferences_language), - message = "", - choices = langMap, - ) - } + uiViewModel.showAlert( + title = context.getString(R.string.preferences_language), + message = "", + choices = langMap, + ) + } - val themeMap = remember { - mapOf( - context.getString(R.string.dynamic) to MODE_DYNAMIC, - context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO, - context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES, - context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, - ) - } - SettingsItem( - text = stringResource(R.string.theme), - leadingIcon = Icons.Rounded.FormatPaint, - trailingIcon = null, - ) { - uiViewModel.showAlert( - title = context.getString(R.string.choose_theme), - message = "", - choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } }, - ) - } - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val themeMap = remember { + mapOf( + context.getString(R.string.dynamic) to MODE_DYNAMIC, + context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO, + context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES, + context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ) + } + SettingsItem( + text = stringResource(R.string.theme), + leadingIcon = Icons.Rounded.FormatPaint, + trailingIcon = null, + ) { + uiViewModel.showAlert( + title = context.getString(R.string.choose_theme), + message = "", + choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } }, + ) + } + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) - val exportRangeTestLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } + val exportRangeTestLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } + } } + SettingsItem( + text = stringResource(R.string.save_rangetest), + leadingIcon = Icons.Rounded.Output, + trailingIcon = null, + ) { + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/csv" + putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv") + } + exportRangeTestLauncher.launch(intent) } - SettingsItem( - text = stringResource(R.string.save_rangetest), - leadingIcon = Icons.Rounded.Output, - trailingIcon = null, - ) { - val intent = - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/csv" - putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv") - } - exportRangeTestLauncher.launch(intent) - } - val exportDataLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } + val exportDataLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) } + } } + SettingsItem( + text = stringResource(R.string.export_data_csv), + leadingIcon = Icons.Rounded.Output, + trailingIcon = null, + ) { + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/csv" + putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv") + } + exportDataLauncher.launch(intent) } - SettingsItem( - text = stringResource(R.string.export_data_csv), - leadingIcon = Icons.Rounded.Output, - trailingIcon = null, - ) { - val intent = - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/csv" - putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv") - } - exportDataLauncher.launch(intent) - } - SettingsItem( - text = stringResource(R.string.intro_show), - leadingIcon = Icons.Rounded.WavingHand, - trailingIcon = null, - ) { - uiViewModel.showAppIntro() - } + SettingsItem( + text = stringResource(R.string.intro_show), + leadingIcon = Icons.Rounded.WavingHand, + trailingIcon = null, + ) { + uiViewModel.showAppIntro() + } - AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() } + AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() } + } } } } From 1927eb7871b000f6c70566354a9dc0a602f55963 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:04:02 -0400 Subject: [PATCH 02/10] Remove ConnectionsScreen global app bar --- .../main/java/com/geeksville/mesh/ui/Main.kt | 4 +- .../mesh/ui/connections/ConnectionsScreen.kt | 303 ++++++++++-------- .../mesh/ui/settings/SettingsScreen.kt | 2 +- 3 files changed, 166 insertions(+), 143 deletions(-) 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 ea2a5ed36e..b1c669506a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -352,7 +352,9 @@ fun MainScreen( fun NavDestination.hasGlobalAppBar(): Boolean = // List of screens to exclude from having the global app bar - listOf>(SettingsRoutes.Settings::class).none { this.hasRoute(it) } + listOf(ConnectionsRoutes.Connections::class, SettingsRoutes.Settings::class).none { + this.hasRoute(it) + } AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) { MainAppBar( diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index aa31e4a9dc..a69207bad6 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -65,6 +66,7 @@ import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.SettingsRoutes import com.geeksville.mesh.navigation.getNavRouteFrom import com.geeksville.mesh.service.ConnectionState +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.connections.components.BLEDevices import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedCard @@ -109,6 +111,7 @@ fun ConnectionsScreen( val scanning by scanModel.spinner.collectAsStateWithLifecycle(false) val context = LocalContext.current val info by connectionsViewModel.myNodeInfo.collectAsStateWithLifecycle() + val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle() val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle() val bluetoothEnabled by bluetoothViewModel.enabled.collectAsStateWithLifecycle(false) val regionUnset = @@ -175,174 +178,192 @@ fun ConnectionsScreen( SharedContactDialog(contact = showSharedContact, onDismiss = { showSharedContact = null }) } - Column(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize().weight(1f)) { - Column( - modifier = Modifier.fillMaxSize().verticalScroll(scrollState).height(IntrinsicSize.Max).padding(16.dp), - ) { - val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle() - - AnimatedVisibility( - visible = connectionState.isConnected(), - modifier = Modifier.padding(bottom = 16.dp), + Scaffold( + topBar = { + MainAppBar( + title = stringResource(R.string.connections), + ourNode = ourNode, + isConnected = connectionState.isConnected(), + showNodeChip = ourNode != null && connectionState.isConnected(), + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = {}, + ) + }, + ) { paddingValues -> + Column(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize().weight(1f)) { + Column( + modifier = + Modifier.fillMaxSize() + .verticalScroll(scrollState) + .height(IntrinsicSize.Max) + .padding(paddingValues) + .padding(16.dp), ) { - Column { - ourNode?.let { node -> - Text( - stringResource(R.string.connected_device), - modifier = Modifier.padding(horizontal = 16.dp), - style = MaterialTheme.typography.titleLarge, - ) + AnimatedVisibility( + visible = connectionState.isConnected(), + modifier = Modifier.padding(bottom = 16.dp), + ) { + Column { + ourNode?.let { node -> + Text( + stringResource(R.string.connected_device), + modifier = Modifier.padding(horizontal = 16.dp), + style = MaterialTheme.typography.titleLarge, + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - CurrentlyConnectedCard( - node = node, - onNavigateToNodeDetails = onNavigateToNodeDetails, - onSetShowSharedContact = { showSharedContact = it }, - onNavigateToSettings = onNavigateToSettings, - onClickDisconnect = { scanModel.disconnect() }, - ) + CurrentlyConnectedCard( + node = node, + onNavigateToNodeDetails = onNavigateToNodeDetails, + onSetShowSharedContact = { showSharedContact = it }, + onNavigateToSettings = onNavigateToSettings, + onClickDisconnect = { scanModel.disconnect() }, + ) + } } } - } - /*val setRegionText = stringResource(id = R.string.set_your_region) - val actionText = stringResource(id = R.string.action_go) - LaunchedEffect(connectionState.isConnected() && regionUnset && selectedDevice != "m") { - if (connectionState.isConnected() && regionUnset && selectedDevice != "m") { - uiViewModel.showSnackBar( - text = setRegionText, - actionLabel = actionText, - onActionPerformed = { - isWaiting = true - radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA) - }, - ) - } - }*/ + /*val setRegionText = stringResource(id = R.string.set_your_region) + val actionText = stringResource(id = R.string.action_go) + LaunchedEffect(connectionState.isConnected() && regionUnset && selectedDevice != "m") { + if (connectionState.isConnected() && regionUnset && selectedDevice != "m") { + uiViewModel.showSnackBar( + text = setRegionText, + actionLabel = actionText, + onActionPerformed = { + isWaiting = true + radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA) + }, + ) + } + }*/ - var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) } - LaunchedEffect(selectedDevice) { - DeviceType.fromAddress(selectedDevice)?.let { type -> selectedDeviceType = type } - } + var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) } + LaunchedEffect(selectedDevice) { + DeviceType.fromAddress(selectedDevice)?.let { type -> selectedDeviceType = type } + } - ConnectionsSegmentedBar(modifier = Modifier.fillMaxWidth()) { selectedDeviceType = it } + ConnectionsSegmentedBar(modifier = Modifier.fillMaxWidth()) { selectedDeviceType = it } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) - Column(modifier = Modifier.fillMaxSize()) { - when (selectedDeviceType) { - DeviceType.BLE -> { - BLEDevices( - connectionState = connectionState, - btDevices = bleDevices, - selectedDevice = selectedDevice, - scanModel = scanModel, - bluetoothEnabled = bluetoothEnabled, - ) - } + Column(modifier = Modifier.fillMaxSize()) { + when (selectedDeviceType) { + DeviceType.BLE -> { + BLEDevices( + connectionState = connectionState, + btDevices = bleDevices, + selectedDevice = selectedDevice, + scanModel = scanModel, + bluetoothEnabled = bluetoothEnabled, + ) + } - DeviceType.TCP -> { - NetworkDevices( - connectionState = connectionState, - discoveredNetworkDevices = discoveredTcpDevices, - recentNetworkDevices = recentTcpDevices, - selectedDevice = selectedDevice, - scanModel = scanModel, - ) - } + DeviceType.TCP -> { + NetworkDevices( + connectionState = connectionState, + discoveredNetworkDevices = discoveredTcpDevices, + recentNetworkDevices = recentTcpDevices, + selectedDevice = selectedDevice, + scanModel = scanModel, + ) + } - DeviceType.USB -> { - UsbDevices( - connectionState = connectionState, - usbDevices = usbDevices, - selectedDevice = selectedDevice, - scanModel = scanModel, - ) + DeviceType.USB -> { + UsbDevices( + connectionState = connectionState, + usbDevices = usbDevices, + selectedDevice = selectedDevice, + scanModel = scanModel, + ) + } } - } - - Spacer(modifier = Modifier.height(16.dp)) - // Warning Not Paired - val hasShownNotPairedWarning by - connectionsViewModel.hasShownNotPairedWarning.collectAsStateWithLifecycle() - val showWarningNotPaired = - !connectionState.isConnected() && - !hasShownNotPairedWarning && - bleDevices.none { it is DeviceListEntry.Ble && it.bonded } - if (showWarningNotPaired) { - Text( - text = stringResource(R.string.warning_not_paired), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(horizontal = 16.dp), - ) Spacer(modifier = Modifier.height(16.dp)) - LaunchedEffect(Unit) { connectionsViewModel.suppressNoPairedWarning() } + // Warning Not Paired + val hasShownNotPairedWarning by + connectionsViewModel.hasShownNotPairedWarning.collectAsStateWithLifecycle() + val showWarningNotPaired = + !connectionState.isConnected() && + !hasShownNotPairedWarning && + bleDevices.none { it is DeviceListEntry.Ble && it.bonded } + if (showWarningNotPaired) { + Text( + text = stringResource(R.string.warning_not_paired), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + + LaunchedEffect(Unit) { connectionsViewModel.suppressNoPairedWarning() } + } } } - } - // Compose Device Scan Dialog - if (showScanDialog) { - Dialog( - onDismissRequest = { - showScanDialog = false - scanModel.clearScanResults() - }, - ) { - Surface(shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Select a Bluetooth device", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(bottom = 16.dp), - ) - Column(modifier = Modifier.selectableGroup()) { - scanResults.values.forEach { device -> - Row( - modifier = - Modifier.fillMaxWidth() - .selectable( - selected = false, // No pre-selection in this dialog - onClick = { - scanModel.onSelected(device) - scanModel.clearScanResults() - showScanDialog = false - }, - ) - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = device.name) + // Compose Device Scan Dialog + if (showScanDialog) { + Dialog( + onDismissRequest = { + showScanDialog = false + scanModel.clearScanResults() + }, + ) { + Surface(shape = MaterialTheme.shapes.medium) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Select a Bluetooth device", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 16.dp), + ) + Column(modifier = Modifier.selectableGroup()) { + scanResults.values.forEach { device -> + Row( + modifier = + Modifier.fillMaxWidth() + .selectable( + selected = false, // No pre-selection in this dialog + onClick = { + scanModel.onSelected(device) + scanModel.clearScanResults() + showScanDialog = false + }, + ) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = device.name) + } } } - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { - scanModel.clearScanResults() - showScanDialog = false - }, - ) { - Text(stringResource(R.string.cancel)) + Spacer(modifier = Modifier.height(16.dp)) + TextButton( + onClick = { + scanModel.clearScanResults() + showScanDialog = false + }, + ) { + Text(stringResource(R.string.cancel)) + } } } } } } - } - Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Text( - text = scanStatusText.orEmpty(), - modifier = Modifier.fillMaxWidth(), - fontSize = 10.sp, - textAlign = TextAlign.End, - ) + Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Text( + text = scanStatusText.orEmpty(), + modifier = Modifier.fillMaxWidth(), + fontSize = 10.sp, + textAlign = TextAlign.End, + ) + } } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index aa66bb40ef..6e4635cdba 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -170,7 +170,7 @@ fun SettingsScreen( title = stringResource(R.string.bottom_nav_settings), ourNode = ourNode, isConnected = isConnected, - showNodeChip = true, + showNodeChip = ourNode != null && isConnected, canNavigateUp = false, onNavigateUp = {}, actions = {}, From 4d3161c46d6f57e91da8b2ed34074dc302172551 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:19:18 -0400 Subject: [PATCH 03/10] Remove NodeScreen global app bar --- .../main/java/com/geeksville/mesh/ui/Main.kt | 9 ++++++--- .../mesh/ui/common/components/MainAppBar.kt | 11 +---------- .../com/geeksville/mesh/ui/node/NodeScreen.kt | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 13 deletions(-) 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 b1c669506a..0e22635076 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -352,9 +352,12 @@ fun MainScreen( fun NavDestination.hasGlobalAppBar(): Boolean = // List of screens to exclude from having the global app bar - listOf(ConnectionsRoutes.Connections::class, SettingsRoutes.Settings::class).none { - this.hasRoute(it) - } + listOf( + ConnectionsRoutes.Connections::class, + NodesRoutes.Nodes::class, + SettingsRoutes.Settings::class, + ) + .none { this.hasRoute(it) } AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) { MainAppBar( diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt index 99f4543586..c10a342a85 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt @@ -75,8 +75,6 @@ fun MainAppBar( } val longTitle by viewModel.title.collectAsStateWithLifecycle("") - val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0) - val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0) val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle() val isConnected by viewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false) @@ -95,17 +93,10 @@ fun MainAppBar( else -> stringResource(id = R.string.app_name) } - val subtitle = - if (currentDestination?.hasRoute() == true) { - stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount) - } else { - null - } - MainAppBar( modifier = modifier, title = title, - subtitle = subtitle, + subtitle = null, canNavigateUp = navController.previousBackStackEntry != null && currentDestination?.isTopLevel() == false, ourNode = ourNode, isConnected = isConnected, diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index b7165b7f75..ef3f46786d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -42,14 +42,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.R import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.ConnectionState +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.common.components.rememberTimeTickWithLifecycle import com.geeksville.mesh.ui.node.components.NodeFilterTextField import com.geeksville.mesh.ui.node.components.NodeItem @@ -70,6 +73,8 @@ fun NodeScreen( val nodes by model.nodeList.collectAsStateWithLifecycle() val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle() + val onlineNodeCount by model.onlineNodeCount.collectAsStateWithLifecycle(0) + val totalNodeCount by model.totalNodeCount.collectAsStateWithLifecycle(0) val unfilteredNodes by model.unfilteredNodeList.collectAsStateWithLifecycle() val ignoredNodeCount = unfilteredNodes.count { it.isIgnored } @@ -85,6 +90,19 @@ fun NodeScreen( val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } } Scaffold( + topBar = { + MainAppBar( + title = stringResource(R.string.nodes), + subtitle = stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount), + ourNode = ourNode, + isConnected = connectionState.isConnected(), + showNodeChip = false, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = {}, + ) + }, floatingActionButton = { val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0") val shareCapable = firmwareVersion.supportsQrCodeSharing() From 2ac0f6e9a3af134d81d63b823455b9e9ef77503a Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:33:09 -0400 Subject: [PATCH 04/10] Remove ContactsScreen global app bar --- .../main/java/com/geeksville/mesh/ui/Main.kt | 1 + .../geeksville/mesh/ui/contact/Contacts.kt | 58 +++++++++++-------- 2 files changed, 36 insertions(+), 23 deletions(-) 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 0e22635076..23daf68bd0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -354,6 +354,7 @@ fun MainScreen( // List of screens to exclude from having the global app bar listOf( ConnectionsRoutes.Connections::class, + ContactsRoutes.Contacts::class, NodesRoutes.Nodes::class, SettingsRoutes.Settings::class, ) diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt index f1c72d5cb0..88caa05044 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt @@ -18,7 +18,6 @@ package com.geeksville.mesh.ui.contact import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -66,6 +65,7 @@ import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.R import com.geeksville.mesh.model.Contact import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.common.components.MainAppBar import java.util.concurrent.TimeUnit @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -78,6 +78,7 @@ fun ContactsScreen( onNavigateToShare: () -> Unit, ) { val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle() + val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() var showMuteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) } @@ -139,6 +140,27 @@ fun ContactsScreen( } Scaffold( topBar = { + MainAppBar( + title = stringResource(R.string.conversations), + ourNode = ourNode, + isConnected = isConnected, + showNodeChip = ourNode != null && isConnected, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = {}, + ) + }, + floatingActionButton = { + FloatingActionButton( + modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd), + onClick = onNavigateToShare, + ) { + Icon(Icons.Rounded.QrCode2, contentDescription = null) + } + }, + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { if (isSelectionModeActive) { // Display selection toolbar when in selection mode SelectionToolbar( @@ -153,26 +175,17 @@ fun ContactsScreen( isAllMuted = isAllMuted, // Pass the derived state ) } - }, - floatingActionButton = { - FloatingActionButton( - modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd), - onClick = onNavigateToShare, - ) { - Icon(Icons.Rounded.QrCode2, contentDescription = null) - } - }, - ) { paddingValues -> - val channels by uiViewModel.channels.collectAsStateWithLifecycle() - ContactListView( - contacts = contacts, - selectedList = selectedContactKeys, - onClick = onContactClick, - onLongClick = onContactLongClick, - contentPadding = paddingValues, - channels = channels, - onNodeChipClick = onNodeChipClick, - ) + + val channels by uiViewModel.channels.collectAsStateWithLifecycle() + ContactListView( + contacts = contacts, + selectedList = selectedContactKeys, + onClick = onContactClick, + onLongClick = onContactLongClick, + channels = channels, + onNodeChipClick = onNodeChipClick, + ) + } } DeleteConfirmationDialog( showDialog = showDeleteDialog, @@ -351,12 +364,11 @@ fun ContactListView( selectedList: List, onClick: (Contact) -> Unit, onLongClick: (Contact) -> Unit, - contentPadding: PaddingValues, channels: AppOnlyProtos.ChannelSet? = null, onNodeChipClick: (Contact) -> Unit, ) { val haptics = LocalHapticFeedback.current - LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = contentPadding) { + LazyColumn(modifier = Modifier.fillMaxSize()) { items(contacts, key = { it.contactKey }) { contact -> val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } } From 45aea534e855b5951ed06526bf8f2ef2d14d7f19 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:44:39 -0400 Subject: [PATCH 05/10] Remove MapView global app bar --- .../java/com/geeksville/mesh/ui/map/MapView.kt | 16 +++++++++++++++- app/src/main/java/com/geeksville/mesh/ui/Main.kt | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index 24b03c1e30..48bad192a5 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -74,6 +74,7 @@ import com.geeksville.mesh.android.BuildUtils.warn import com.geeksville.mesh.copy import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog import com.geeksville.mesh.ui.map.components.CustomMapLayersSheet import com.geeksville.mesh.ui.map.components.CustomTileProviderManagerSheet @@ -376,7 +377,20 @@ fun MapView( } } - Scaffold { paddingValues -> + Scaffold( + topBar = { + MainAppBar( + title = stringResource(R.string.map), + ourNode = ourNodeInfo, + isConnected = isConnected, + showNodeChip = ourNodeInfo != null && isConnected, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = {}, + ) + }, + ) { paddingValues -> Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { GoogleMap( mapColorScheme = mapColorScheme, 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 23daf68bd0..ca0a375b49 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -355,6 +355,7 @@ fun MainScreen( listOf( ConnectionsRoutes.Connections::class, ContactsRoutes.Contacts::class, + MapRoutes.Map::class, NodesRoutes.Nodes::class, SettingsRoutes.Settings::class, ) From 383f3eceb10560e78650dead298913edfcf4bbd1 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:58:09 -0400 Subject: [PATCH 06/10] Fix map top bars --- .../com/geeksville/mesh/ui/map/MapView.kt | 16 +------ .../com/geeksville/mesh/ui/node/NodeMap.kt | 40 +++++++++++++++- .../mesh/navigation/MapNavigation.kt | 48 +++++++++++++++++-- .../mesh/navigation/NodesNavigation.kt | 34 ++++++++----- .../main/java/com/geeksville/mesh/ui/Main.kt | 2 + 5 files changed, 107 insertions(+), 33 deletions(-) diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index 48bad192a5..24b03c1e30 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -74,7 +74,6 @@ import com.geeksville.mesh.android.BuildUtils.warn import com.geeksville.mesh.copy import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel -import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog import com.geeksville.mesh.ui.map.components.CustomMapLayersSheet import com.geeksville.mesh.ui.map.components.CustomTileProviderManagerSheet @@ -377,20 +376,7 @@ fun MapView( } } - Scaffold( - topBar = { - MainAppBar( - title = stringResource(R.string.map), - ourNode = ourNodeInfo, - isConnected = isConnected, - showNodeChip = ourNodeInfo != null && isConnected, - canNavigateUp = false, - onNavigateUp = {}, - actions = {}, - onAction = {}, - ) - }, - ) { paddingValues -> + Scaffold { paddingValues -> Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { GoogleMap( mapColorScheme = mapColorScheme, diff --git a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt index 0d6fc6dcc8..3f10d002d3 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/node/NodeMap.kt @@ -17,20 +17,56 @@ package com.geeksville.mesh.ui.node +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.model.UIViewModel +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.map.MapView const val DEG_D = 1e-7 @Composable -fun NodeMapScreen(uiViewModel: UIViewModel, metricsViewModel: MetricsViewModel = hiltViewModel()) { +fun NodeMapScreen( + navController: NavHostController, + uiViewModel: UIViewModel, + metricsViewModel: MetricsViewModel = hiltViewModel(), +) { val state by metricsViewModel.state.collectAsState() val positions = state.positionLogs val destNum = state.node?.num - MapView(uiViewModel = uiViewModel, focusedNodeNum = destNum, nodeTrack = positions, navigateToNodeDetails = {}) + val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle() + + Scaffold( + topBar = { + MainAppBar( + title = state.node?.user?.longName ?: "", + ourNode = ourNodeInfo, + isConnected = isConnected, + showNodeChip = false, + canNavigateUp = true, + onNavigateUp = navController::navigateUp, + actions = {}, + onAction = {}, + ) + }, + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + MapView( + uiViewModel = uiViewModel, + focusedNodeNum = destNum, + nodeTrack = positions, + navigateToNodeDetails = {}, + ) + } + } } diff --git a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt index c9b75373f0..845a42a587 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/MapNavigation.kt @@ -17,18 +17,58 @@ package com.geeksville.mesh.navigation +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.navDeepLink +import com.geeksville.mesh.R import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.map.MapView +import com.geeksville.mesh.ui.node.components.NodeMenuAction fun NavGraphBuilder.mapGraph(navController: NavHostController, uiViewModel: UIViewModel) { composable(deepLinks = listOf(navDeepLink(basePath = "$DEEP_LINK_BASE_URI/map"))) { - MapView( - uiViewModel = uiViewModel, - navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, - ) + val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle() + + Scaffold( + topBar = { + MainAppBar( + title = stringResource(R.string.map), + ourNode = ourNodeInfo, + isConnected = isConnected, + showNodeChip = ourNodeInfo != null && isConnected, + canNavigateUp = false, + onNavigateUp = {}, + actions = {}, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> { + navController.navigate(NodesRoutes.NodeDetailGraph(action.node.num)) { + launchSingleTop = true + restoreState = true + } + } + else -> {} + } + }, + ) + }, + ) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + MapView( + uiViewModel = uiViewModel, + navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, + ) + } + } } } 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 7f87dfebda..a332e67941 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/NodesNavigation.kt @@ -183,7 +183,12 @@ private inline fun NavGraphBuilder.addNodeDetailScreenCompos navController: NavHostController, uiViewModel: UIViewModel, routeInfo: NodeDetailRoute, - crossinline screenContent: @Composable (metricsViewModel: MetricsViewModel, passedUiViewModel: UIViewModel) -> Unit, + crossinline screenContent: + @Composable ( + navController: NavHostController, + metricsViewModel: MetricsViewModel, + passedUiViewModel: UIViewModel, + ) -> Unit, ) { composable( deepLinks = @@ -195,7 +200,7 @@ private inline fun NavGraphBuilder.addNodeDetailScreenCompos val parentGraphBackStackEntry = remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) } val metricsViewModel = hiltViewModel(parentGraphBackStackEntry) - screenContent(metricsViewModel, uiViewModel) + screenContent(navController, metricsViewModel, uiViewModel) } } @@ -203,60 +208,65 @@ enum class NodeDetailRoute( @StringRes val title: Int, val route: Route, val icon: ImageVector?, - val screenComposable: @Composable (metricsViewModel: MetricsViewModel, uiViewModel: UIViewModel) -> Unit, + val screenComposable: + @Composable ( + navController: NavHostController, + metricsViewModel: MetricsViewModel, + uiViewModel: UIViewModel, + ) -> Unit, ) { DEVICE( R.string.device, NodeDetailRoutes.DeviceMetrics, Icons.Default.Router, - { metricsVM, _ -> DeviceMetricsScreen(metricsVM) }, + { _, metricsVM, _ -> DeviceMetricsScreen(metricsVM) }, ), NODE_MAP( R.string.node_map, NodeDetailRoutes.NodeMap, Icons.Default.LocationOn, - { metricsVM, uiVM -> NodeMapScreen(uiVM, metricsVM) }, + { navController, metricsVM, uiVM -> NodeMapScreen(navController, uiVM, metricsVM) }, ), POSITION_LOG( R.string.position_log, NodeDetailRoutes.PositionLog, Icons.Default.LocationOn, - { metricsVM, _ -> PositionLogScreen(metricsVM) }, + { _, metricsVM, _ -> PositionLogScreen(metricsVM) }, ), ENVIRONMENT( R.string.environment, NodeDetailRoutes.EnvironmentMetrics, Icons.Default.LightMode, - { metricsVM, _ -> EnvironmentMetricsScreen(metricsVM) }, + { _, metricsVM, _ -> EnvironmentMetricsScreen(metricsVM) }, ), SIGNAL( R.string.signal, NodeDetailRoutes.SignalMetrics, Icons.Default.CellTower, - { metricsVM, _ -> SignalMetricsScreen(metricsVM) }, + { _, metricsVM, _ -> SignalMetricsScreen(metricsVM) }, ), TRACEROUTE( R.string.traceroute, NodeDetailRoutes.TracerouteLog, Icons.Default.PermScanWifi, - { metricsVM, _ -> TracerouteLogScreen(viewModel = metricsVM) }, + { _, metricsVM, _ -> TracerouteLogScreen(viewModel = metricsVM) }, ), POWER( R.string.power, NodeDetailRoutes.PowerMetrics, Icons.Default.Power, - { metricsVM, _ -> PowerMetricsScreen(metricsVM) }, + { _, metricsVM, _ -> PowerMetricsScreen(metricsVM) }, ), HOST( R.string.host, NodeDetailRoutes.HostMetricsLog, Icons.Default.Memory, - { metricsVM, _ -> HostMetricsLogScreen(metricsVM) }, + { _, metricsVM, _ -> HostMetricsLogScreen(metricsVM) }, ), PAX( R.string.pax, NodeDetailRoutes.PaxMetrics, Icons.Default.People, - { metricsVM, _ -> PaxMetricsScreen(metricsVM) }, + { _, metricsVM, _ -> PaxMetricsScreen(metricsVM) }, ), } 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 ca0a375b49..b11c058cde 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -88,6 +88,7 @@ import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.navigation.ConnectionsRoutes import com.geeksville.mesh.navigation.ContactsRoutes import com.geeksville.mesh.navigation.MapRoutes +import com.geeksville.mesh.navigation.NodeDetailRoutes import com.geeksville.mesh.navigation.NodesRoutes import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.SettingsRoutes @@ -356,6 +357,7 @@ fun MainScreen( ConnectionsRoutes.Connections::class, ContactsRoutes.Contacts::class, MapRoutes.Map::class, + NodeDetailRoutes.NodeMap::class, NodesRoutes.Nodes::class, SettingsRoutes.Settings::class, ) From 15da7410b5b5fa66576e3dc891f026edd844d039 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:21:26 -0400 Subject: [PATCH 07/10] Handle node chip clicks --- .../mesh/navigation/ConnectionsNavigation.kt | 6 ++++++ .../geeksville/mesh/navigation/ContactsNavigation.kt | 6 ++++++ .../geeksville/mesh/navigation/SettingsNavigation.kt | 11 ++++++++++- .../mesh/ui/connections/ConnectionsScreen.kt | 9 ++++++++- .../java/com/geeksville/mesh/ui/contact/Contacts.kt | 9 ++++++++- .../com/geeksville/mesh/ui/settings/SettingsScreen.kt | 9 ++++++++- 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt index 65d69cba7d..d11172362a 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/ConnectionsNavigation.kt @@ -42,6 +42,12 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController, bluetooth ConnectionsScreen( bluetoothViewModel = bluetoothViewModel, radioConfigViewModel = hiltViewModel(parentEntry), + onClickNodeChip = { + navController.navigate(NodesRoutes.NodeDetailGraph(it)) { + launchSingleTop = true + restoreState = true + } + }, onNavigateToSettings = { navController.navigate(SettingsRoutes.Settings()) }, onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, onConfigNavigate = { route -> navController.navigate(route) }, 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 db090d996f..30c1d987f6 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/ContactsNavigation.kt @@ -37,6 +37,12 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel: ) { ContactsScreen( uiViewModel, + onClickNodeChip = { + navController.navigate(NodesRoutes.NodeDetailGraph(it)) { + launchSingleTop = true + restoreState = true + } + }, onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) }, onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) }, onNavigateToShare = { navController.navigate(ChannelsRoutes.ChannelsGraph) }, diff --git a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt index 2d67f645b0..e0d4b9ef79 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt @@ -97,7 +97,16 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController, uiViewModel: ) { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } - SettingsScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) { + SettingsScreen( + uiViewModel = uiViewModel, + viewModel = hiltViewModel(parentEntry), + onClickNodeChip = { + navController.navigate(NodesRoutes.NodeDetailGraph(it)) { + launchSingleTop = true + restoreState = true + } + }, + ) { navController.navigate(it) { popUpTo(SettingsRoutes.Settings()) { inclusive = false } } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index a69207bad6..e0f3a424fd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -72,6 +72,7 @@ import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedCard import com.geeksville.mesh.ui.connections.components.NetworkDevices import com.geeksville.mesh.ui.connections.components.UsbDevices +import com.geeksville.mesh.ui.node.components.NodeMenuAction import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog import com.geeksville.mesh.ui.sharing.SharedContactDialog @@ -97,6 +98,7 @@ fun ConnectionsScreen( scanModel: BTScanModel = hiltViewModel(), bluetoothViewModel: BluetoothViewModel = hiltViewModel(), radioConfigViewModel: RadioConfigViewModel = hiltViewModel(), + onClickNodeChip: (Int) -> Unit, onNavigateToSettings: () -> Unit, onNavigateToNodeDetails: (Int) -> Unit, onConfigNavigate: (Route) -> Unit, @@ -188,7 +190,12 @@ fun ConnectionsScreen( canNavigateUp = false, onNavigateUp = {}, actions = {}, - onAction = {}, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num) + else -> {} + } + }, ) }, ) { paddingValues -> diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt index 88caa05044..5c362044b0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt @@ -66,6 +66,7 @@ import com.geeksville.mesh.R import com.geeksville.mesh.model.Contact import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.common.components.MainAppBar +import com.geeksville.mesh.ui.node.components.NodeMenuAction import java.util.concurrent.TimeUnit @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -73,6 +74,7 @@ import java.util.concurrent.TimeUnit @Composable fun ContactsScreen( uiViewModel: UIViewModel = hiltViewModel(), + onClickNodeChip: (Int) -> Unit = {}, onNavigateToMessages: (String) -> Unit = {}, onNavigateToNodeDetails: (Int) -> Unit = {}, onNavigateToShare: () -> Unit, @@ -148,7 +150,12 @@ fun ContactsScreen( canNavigateUp = false, onNavigateUp = {}, actions = {}, - onAction = {}, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num) + else -> {} + } + }, ) }, floatingActionButton = { diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt index 6e4635cdba..e4445749d1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -63,6 +63,7 @@ import com.geeksville.mesh.navigation.getNavRouteFrom import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.common.components.TitledCard import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC +import com.geeksville.mesh.ui.node.components.NodeMenuAction import com.geeksville.mesh.ui.settings.components.SettingsItem import com.geeksville.mesh.ui.settings.components.SettingsItemDetail import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch @@ -85,6 +86,7 @@ import kotlin.time.Duration.Companion.seconds fun SettingsScreen( viewModel: RadioConfigViewModel = hiltViewModel(), uiViewModel: UIViewModel = hiltViewModel(), + onClickNodeChip: (Int) -> Unit = {}, onNavigate: (Route) -> Unit = {}, ) { val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() @@ -174,7 +176,12 @@ fun SettingsScreen( canNavigateUp = false, onNavigateUp = {}, actions = {}, - onAction = {}, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num) + else -> {} + } + }, ) }, ) { paddingValues -> From f62fbf1c51f4afd108ab31f2cd84bc349ff53e42 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:41:44 -0400 Subject: [PATCH 08/10] Remove NodeDetailScreen global app bar --- .../main/java/com/geeksville/mesh/ui/Main.kt | 1 + .../com/geeksville/mesh/ui/node/NodeDetail.kt | 72 ++++++++++++------- 2 files changed, 49 insertions(+), 24 deletions(-) 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 b11c058cde..db3ef6fad9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -359,6 +359,7 @@ fun MainScreen( MapRoutes.Map::class, NodeDetailRoutes.NodeMap::class, NodesRoutes.Nodes::class, + NodesRoutes.NodeDetail::class, SettingsRoutes.Settings::class, ) .none { this.hasRoute(it) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt index 432462c177..ed15b28fec 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt @@ -92,6 +92,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -138,6 +139,7 @@ import com.geeksville.mesh.navigation.NodeDetailRoutes import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.SettingsRoutes import com.geeksville.mesh.service.ServiceAction +import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.common.components.TitledCard import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider import com.geeksville.mesh.ui.common.theme.AppTheme @@ -176,12 +178,13 @@ private data class DrawableMetricInfo( val rotateIcon: Float = 0f, ) +@Suppress("LongMethod") @Composable fun NodeDetailScreen( modifier: Modifier = Modifier, viewModel: MetricsViewModel = hiltViewModel(), uiViewModel: UIViewModel = hiltViewModel(), - navigateToMessages: (String) -> Unit, + navigateToMessages: (String) -> Unit = {}, onNavigate: (Route) -> Unit = {}, onNavigateUp: () -> Unit = {}, ) { @@ -189,6 +192,7 @@ fun NodeDetailScreen( val environmentState by viewModel.environmentState.collectAsStateWithLifecycle() val lastTracerouteTime by uiViewModel.lastTraceRouteTime.collectAsStateWithLifecycle() val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() + val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false) val availableLogs by remember(state, environmentState) { @@ -210,29 +214,49 @@ fun NodeDetailScreen( } val node = state.node - if (node != null) { - NodeDetailContent( - node = node, - ourNode = ourNode, - metricsState = state, - lastTracerouteTime = lastTracerouteTime, - availableLogs = availableLogs, - uiViewModel = uiViewModel, - onAction = { action -> - handleNodeAction( - action = action, - uiViewModel = uiViewModel, - node = node, - navigateToMessages = navigateToMessages, - onNavigateUp = onNavigateUp, - onNavigate = onNavigate, - viewModel = viewModel, - ) - }, - modifier = modifier, - ) - } else { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } + + @Suppress("ModifierNotUsedAtRoot") + Scaffold( + topBar = { + MainAppBar( + title = node?.user?.longName ?: "", + ourNode = ourNode, + isConnected = isConnected, + showNodeChip = false, + canNavigateUp = true, + onNavigateUp = onNavigateUp, + actions = {}, + onAction = {}, + ) + }, + ) { paddingValues -> + if (node != null) { + @Suppress("ViewModelForwarding") + NodeDetailContent( + node = node, + ourNode = ourNode, + metricsState = state, + lastTracerouteTime = lastTracerouteTime, + availableLogs = availableLogs, + uiViewModel = uiViewModel, + onAction = { action -> + handleNodeAction( + action = action, + uiViewModel = uiViewModel, + node = node, + navigateToMessages = navigateToMessages, + onNavigateUp = onNavigateUp, + onNavigate = onNavigate, + viewModel = viewModel, + ) + }, + modifier = modifier.padding(paddingValues), + ) + } else { + Box(modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } } } From 19436f497e2162f9e0cd1afad427c76f5035f5dc Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:12:17 -0400 Subject: [PATCH 09/10] Post merge main fixes --- .../mesh/ui/connections/ConnectionsScreen.kt | 37 +++---- .../components/ConnectionsSegmentedBar.kt | 21 ++-- ...ectedCard.kt => CurrentlyConnectedInfo.kt} | 103 +++++++++--------- .../ui/connections/components/UsbDevices.kt | 2 +- 4 files changed, 77 insertions(+), 86 deletions(-) rename app/src/main/java/com/geeksville/mesh/ui/connections/components/{CurrentlyConnectedCard.kt => CurrentlyConnectedInfo.kt} (58%) diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt index 7c3af934a4..70d4855add 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt @@ -74,11 +74,11 @@ import com.geeksville.mesh.ui.common.components.MainAppBar import com.geeksville.mesh.ui.common.components.TitledCard import com.geeksville.mesh.ui.connections.components.BLEDevices import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar -import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedCard +import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo import com.geeksville.mesh.ui.connections.components.NetworkDevices import com.geeksville.mesh.ui.connections.components.UsbDevices -import com.geeksville.mesh.ui.settings.components.SettingsItem import com.geeksville.mesh.ui.node.components.NodeMenuAction +import com.geeksville.mesh.ui.settings.components.SettingsItem import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog import com.geeksville.mesh.ui.sharing.SharedContactDialog @@ -219,19 +219,15 @@ fun ConnectionsScreen( ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { ourNode?.let { node -> - Text( - stringResource(R.string.connected_device), - modifier = Modifier.padding(horizontal = 16.dp), - style = MaterialTheme.typography.titleLarge, - ) - - CurrentlyConnectedCard( - node = node, - onNavigateToNodeDetails = onNavigateToNodeDetails, - onSetShowSharedContact = { showSharedContact = it }, - onNavigateToSettings = onNavigateToSettings, - onClickDisconnect = { scanModel.disconnect() }, - ) + TitledCard(title = stringResource(R.string.connected_device)) { + CurrentlyConnectedInfo( + node = node, + onNavigateToNodeDetails = onNavigateToNodeDetails, + onSetShowSharedContact = { showSharedContact = it }, + onNavigateToSettings = onNavigateToSettings, + onClickDisconnect = { scanModel.disconnect() }, + ) + } } if (regionUnset && selectedDevice != "m") { @@ -249,11 +245,14 @@ fun ConnectionsScreen( } var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) } - LaunchedEffect(selectedDevice) { - DeviceType.fromAddress(selectedDevice)?.let { type -> selectedDeviceType = type } - } + LaunchedEffect(Unit) { DeviceType.fromAddress(selectedDevice)?.let { selectedDeviceType = it } } - ConnectionsSegmentedBar(modifier = Modifier.fillMaxWidth()) { selectedDeviceType = it } + ConnectionsSegmentedBar( + selectedDeviceType = selectedDeviceType, + modifier = Modifier.fillMaxWidth(), + ) { + selectedDeviceType = it + } Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/ConnectionsSegmentedBar.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/ConnectionsSegmentedBar.kt index c33f8f1ec4..dcd87843fc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/ConnectionsSegmentedBar.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/ConnectionsSegmentedBar.kt @@ -28,10 +28,6 @@ import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -43,19 +39,18 @@ import com.geeksville.mesh.ui.connections.DeviceType @Suppress("LambdaParameterEventTrailing") @Composable -fun ConnectionsSegmentedBar(modifier: Modifier = Modifier, onClickDeviceType: (DeviceType) -> Unit) { - var selectedItem by remember { mutableStateOf(Item.BLUETOOTH) } - +fun ConnectionsSegmentedBar( + selectedDeviceType: DeviceType, + modifier: Modifier = Modifier, + onClickDeviceType: (DeviceType) -> Unit, +) { SingleChoiceSegmentedButtonRow(modifier = modifier) { Item.entries.forEachIndexed { index, item -> val text = stringResource(item.textRes) SegmentedButton( shape = SegmentedButtonDefaults.itemShape(index, Item.entries.size), - onClick = { - selectedItem = item - onClickDeviceType(item.deviceType) - }, - selected = item == selectedItem, + onClick = { onClickDeviceType(item.deviceType) }, + selected = item.deviceType == selectedDeviceType, icon = { Icon(imageVector = item.imageVector, contentDescription = text) }, label = { Text(text = text, maxLines = 1, overflow = TextOverflow.Ellipsis) }, ) @@ -72,5 +67,5 @@ private enum class Item(val imageVector: ImageVector, @StringRes val textRes: In @Preview(showBackground = true) @Composable private fun ConnectionsSegmentedBarPreview() { - AppTheme { ConnectionsSegmentedBar {} } + AppTheme { ConnectionsSegmentedBar(selectedDeviceType = DeviceType.BLE) {} } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedCard.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt similarity index 58% rename from app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedCard.kt rename to app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt index 0bd09b4479..7ffc41e41c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedCard.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt @@ -27,7 +27,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -53,81 +52,79 @@ import com.geeksville.mesh.ui.node.components.NodeChip import com.geeksville.mesh.ui.node.components.NodeMenuAction @Composable -fun CurrentlyConnectedCard( +fun CurrentlyConnectedInfo( node: Node, - modifier: Modifier = Modifier, onNavigateToNodeDetails: (Int) -> Unit, onSetShowSharedContact: (Node) -> Unit, onNavigateToSettings: () -> Unit, onClickDisconnect: () -> Unit, + modifier: Modifier = Modifier, ) { - Card(modifier = modifier) { - Column { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - NodeChip( - node = node, - isThisNode = true, - isConnected = true, - onAction = { action -> - when (action) { - is NodeMenuAction.MoreDetails -> onNavigateToNodeDetails(node.num) - - is NodeMenuAction.Share -> onSetShowSharedContact(node) - else -> {} - } - }, - ) + Column(modifier = modifier) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NodeChip( + node = node, + isThisNode = true, + isConnected = true, + onAction = { action -> + when (action) { + is NodeMenuAction.MoreDetails -> onNavigateToNodeDetails(node.num) - MaterialBatteryInfo(level = node.batteryLevel) - } + is NodeMenuAction.Share -> onSetShowSharedContact(node) + else -> {} + } + }, + ) - Column(modifier = Modifier.weight(1f, fill = true)) { - Text(text = node.user.longName, style = MaterialTheme.typography.titleMedium) + MaterialBatteryInfo(level = node.batteryLevel) + } - node.metadata?.firmwareVersion?.let { firmwareVersion -> - Text( - text = stringResource(R.string.firmware_version, firmwareVersion), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } + Column(modifier = Modifier.weight(1f, fill = true)) { + Text(text = node.user.longName, style = MaterialTheme.typography.titleMedium) - IconButton(enabled = true, onClick = onNavigateToSettings) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(id = R.string.radio_configuration), + node.metadata?.firmwareVersion?.let { firmwareVersion -> + Text( + text = stringResource(R.string.firmware_version, firmwareVersion), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } - Button( - shape = RectangleShape, - modifier = Modifier.fillMaxWidth().height(40.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.StatusRed, - contentColor = Color.White, - ), - onClick = onClickDisconnect, - ) { - Text(stringResource(R.string.disconnect)) + IconButton(enabled = true, onClick = onNavigateToSettings) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(id = R.string.radio_configuration), + ) } } + + Button( + shape = RectangleShape, + modifier = Modifier.fillMaxWidth().height(40.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.StatusRed, + contentColor = Color.White, + ), + onClick = onClickDisconnect, + ) { + Text(stringResource(R.string.disconnect)) + } } } @Suppress("MagicNumber") @PreviewLightDark @Composable -private fun CurrentlyConnectedCardPreview() { +private fun CurrentlyConnectedInfoPreview() { AppTheme { - CurrentlyConnectedCard( + CurrentlyConnectedInfo( node = Node( num = 13444, diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt index 2dfdddd539..55b71a79cd 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt @@ -57,7 +57,7 @@ private fun UsbDevices( EmptyStateContent(imageVector = Icons.Rounded.UsbOff, text = stringResource(R.string.no_usb_devices)) else -> - TitledCard(title = "") { + TitledCard(title = null) { usbDevices.forEach { device -> DeviceListItem( connected = From 8b3f3b0d3d1f7f5815b64abb71715db7808e163a Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Wed, 17 Sep 2025 18:29:09 -0400 Subject: [PATCH 10/10] Fix compilation --- app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt index b9e9051ed9..45820aa3a6 100644 --- a/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt +++ b/app/src/fdroid/java/com/geeksville/mesh/ui/node/NodeMap.kt @@ -26,6 +26,7 @@ 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.model.UIViewModel import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle @@ -40,6 +41,7 @@ private const val DEG_D = 1e-7 @Composable fun NodeMapScreen( + navController: NavHostController, @Suppress("UNUSED_PARAMETER") uiViewModel: UIViewModel = hiltViewModel(), viewModel: MetricsViewModel = hiltViewModel(), ) {