diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index fe9d5f933..7f72cd9a8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.SharedPreferences import android.hardware.usb.UsbManager import android.os.RemoteException +import androidx.core.content.edit import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -51,8 +52,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject import org.json.JSONArray +import javax.inject.Inject @HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") @@ -126,7 +127,7 @@ class BTScanModel @Inject constructor( } // Include saved IP connections - recent?.forEach { address -> + recent.forEach { address -> addDevice(DeviceListEntry(context.getString(R.string.meshtastic), address, true)) } @@ -303,9 +304,9 @@ class BTScanModel @Inject constructor( } private fun setRecentAddresses(addresses: List) { - val editor = preferences.edit() - editor.putString("recent-ip-addresses", addresses.toString()) - editor.apply() + preferences.edit { + putString("recent-ip-addresses", addresses.toString()) + } recentIpAddresses.value = addresses } 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 d5eb43e25..6d8699f4b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -473,7 +473,7 @@ private fun TopBarActions( ) { val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle() val isConnected by viewModel.isConnected.collectAsStateWithLifecycle(false) - AnimatedVisibility(ourNode != null && currentDestination?.isTopLevel() == true) { + AnimatedVisibility(ourNode != null && currentDestination?.isTopLevel() == true && isConnected) { ourNode?.let { NodeChip( node = it, diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt index 6816e627d..61651a3e6 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/Connections.kt @@ -42,17 +42,14 @@ import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Usb import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Checkbox -import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SegmentedButton @@ -320,22 +317,6 @@ fun ConnectionsScreen( contentDescription = stringResource(id = R.string.radio_configuration) ) } - FilledIconButton( - colors = IconButtonDefaults.filledIconButtonColors().copy( - containerColor = MaterialTheme.colorScheme.error - ), - enabled = true, - onClick = { - devices.values.find { it.isDisconnect }?.let { - scanModel.onSelected(it) - } - } - ) { - Icon( - imageVector = Icons.Default.CloudOff, - contentDescription = stringResource(id = R.string.disconnect), - ) - } } } Spacer(modifier = Modifier.height(8.dp)) @@ -357,12 +338,11 @@ fun ConnectionsScreen( } } } - var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) } LaunchedEffect(selectedDevice) { - // Determine based on the selected device - if disconnected, keep the last selected type - selectedDeviceType = - selectedDevice.let { DeviceType.fromAddress(it) } ?: selectedDeviceType + DeviceType.fromAddress(selectedDevice)?.let { type -> + selectedDeviceType = type + } } SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth(), @@ -436,7 +416,8 @@ fun ConnectionsScreen( DeviceType.BLE -> { BLEDevices( - devices.values.filter { it.isBLE }, + connectionState, + devices.values.filter { it.isBLE || it.isDisconnect }, selectedDevice, showBluetoothRationaleDialog = { showBluetoothRationaleDialog = true @@ -452,7 +433,8 @@ fun ConnectionsScreen( DeviceType.TCP -> { NetworkDevices( - devices.values.filter { it.isTCP }, + connectionState, + devices.values.filter { it.isTCP || it.isDisconnect }, selectedDevice, scanModel ) @@ -460,7 +442,8 @@ fun ConnectionsScreen( DeviceType.USB -> { UsbDevices( - devices.values.filter { it.isUSB || it.isMock }, + connectionState, + devices.values.filter { it.isUSB || it.isDisconnect || it.isMock }, selectedDevice, scanModel ) @@ -723,7 +706,6 @@ private enum class DeviceType { val isUSB: Boolean = prefix == 's' val isTCP: Boolean = prefix == 't' val isMock: Boolean = prefix == 'm' - val isDisconnect: Boolean = prefix == 'n' return when { isBLE -> BLE isUSB -> USB diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt index c4276f14a..7b13c3300 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt @@ -19,8 +19,6 @@ package com.geeksville.mesh.ui.connections.components import android.app.Activity import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -44,10 +42,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.R import com.geeksville.mesh.android.getBluetoothPermissions import com.geeksville.mesh.model.BTScanModel +import com.geeksville.mesh.service.MeshService @Suppress("LongMethod") @Composable fun BLEDevices( + connectionState: MeshService.ConnectionState, btDevices: List, selectedDevice: String, showBluetoothRationaleDialog: () -> Unit, @@ -56,24 +56,21 @@ fun BLEDevices( ) { val context = LocalContext.current val isScanning by scanModel.spinner.collectAsStateWithLifecycle(false) - Row { - Text( - text = stringResource(R.string.bluetooth), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(vertical = 8.dp) - ) - Spacer(modifier = Modifier.weight(1f)) - } - if (btDevices.isNotEmpty()) { - btDevices.forEach { device -> - DeviceListItem( - device, - device.fullAddress == selectedDevice - ) { - scanModel.onSelected(device) - } + Text( + text = stringResource(R.string.bluetooth), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(vertical = 8.dp) + ) + btDevices.forEach { device -> + DeviceListItem( + connectionState, + device, + device.fullAddress == selectedDevice + ) { + scanModel.onSelected(device) } - } else if (isScanning) { + } + if (isScanning) { Column( modifier = Modifier .fillMaxWidth() @@ -89,7 +86,7 @@ fun BLEDevices( modifier = Modifier.padding(vertical = 8.dp) ) } - } else { + } else if (btDevices.filterNot { it.isDisconnect }.isEmpty()) { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt index 07b71e190..a4e3cad8d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/DeviceListItem.kt @@ -23,12 +23,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.CloudDone +import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.CloudQueue import androidx.compose.material.icons.filled.Usb import androidx.compose.material.icons.filled.Wifi -import androidx.compose.material.icons.outlined.CloudDone import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -36,9 +39,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import com.geeksville.mesh.R import com.geeksville.mesh.model.BTScanModel +import com.geeksville.mesh.service.MeshService +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun DeviceListItem( + connectionState: MeshService.ConnectionState, device: BTScanModel.DeviceListEntry, selected: Boolean, onSelect: () -> Unit, @@ -49,7 +55,7 @@ fun DeviceListItem( Icons.Default.Usb } else if (device.isTCP) { Icons.Default.Wifi - } else if (device.isDisconnect) { + } else if (device.isDisconnect) { // This is the "Disconnect" entry type Icons.Default.Cancel } else { Icons.Default.Add @@ -61,12 +67,41 @@ fun DeviceListItem( stringResource(R.string.serial) } else if (device.isTCP) { stringResource(R.string.network) - } else if (device.isDisconnect) { + } else if (device.isDisconnect) { // This is the "Disconnect" entry type stringResource(R.string.disconnect) } else { stringResource(R.string.add) } + val colors = when { + selected && device.isDisconnect -> { + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.errorContainer, + headlineColor = MaterialTheme.colorScheme.onErrorContainer, + leadingIconColor = MaterialTheme.colorScheme.onErrorContainer, + supportingColor = MaterialTheme.colorScheme.onErrorContainer, + trailingIconColor = MaterialTheme.colorScheme.onErrorContainer, + ) + } + + selected -> { // Standard selection for other device types + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + headlineColor = MaterialTheme.colorScheme.onPrimaryContainer, + leadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + trailingIconColor = when (connectionState) { + MeshService.ConnectionState.CONNECTED -> Color(color = 0xFF30C047) + MeshService.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onPrimaryContainer // Fallback for other states (e.g. connecting) + }, + ) + } + + else -> { + ListItemDefaults.colors() + } + } + ListItem( modifier = Modifier .fillMaxWidth() @@ -77,7 +112,7 @@ fun DeviceListItem( headlineContent = { Text(device.name) }, leadingContent = { Icon( - icon, + icon, // icon is already CloudOff if device.isDisconnect contentDescription ) }, @@ -87,18 +122,23 @@ fun DeviceListItem( } }, trailingContent = { - if (selected) { + if (device.isDisconnect) { Icon( - Icons.Outlined.CloudDone, - stringResource(R.string.connected), - tint = Color(color = 0xFF30C047) + imageVector = Icons.Default.CloudOff, + contentDescription = stringResource(R.string.disconnect), + ) + } else if (connectionState == MeshService.ConnectionState.CONNECTED) { + Icon( + imageVector = Icons.Default.CloudDone, + contentDescription = stringResource(R.string.connected), ) } else { Icon( - Icons.Default.CloudQueue, - stringResource(R.string.not_connected) + imageVector = Icons.Default.CloudQueue, + contentDescription = stringResource(R.string.not_connected), ) } - } + }, + colors = colors ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt index ee9500712..41dfc0914 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt @@ -45,30 +45,31 @@ import androidx.compose.ui.unit.dp import com.geeksville.mesh.R import com.geeksville.mesh.model.BTScanModel import com.geeksville.mesh.repository.network.NetworkRepository +import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.ui.connections.isIPAddress @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("MagicNumber", "LongMethod") @Composable fun NetworkDevices( + connectionState: MeshService.ConnectionState, networkDevices: List, selectedDevice: String, scanModel: BTScanModel, ) { val manualIpAddress = rememberTextFieldState("") val manualIpPort = rememberTextFieldState(NetworkRepository.Companion.SERVICE_PORT.toString()) - if (networkDevices.isNotEmpty()) { - Text( - text = stringResource(R.string.network), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(vertical = 8.dp) - ) - networkDevices.forEach { device -> - DeviceListItem(device, device.fullAddress == selectedDevice) { - scanModel.onSelected(device) - } + Text( + text = stringResource(R.string.network), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(vertical = 8.dp) + ) + networkDevices.forEach { device -> + DeviceListItem(connectionState, device, device.fullAddress == selectedDevice) { + scanModel.onSelected(device) } - } else { + } + if (networkDevices.filterNot { it.isDisconnect }.isEmpty()) { Column( modifier = Modifier .fillMaxWidth() @@ -93,7 +94,7 @@ fun NetworkDevices( .fillMaxWidth() .padding(8.dp), verticalAlignment = Alignment.Companion.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + horizontalArrangement = Arrangement.spacedBy(8.dp, CenterHorizontally) ) { OutlinedTextField( state = manualIpAddress, 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 1ae93a286..a1703eb2e 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 @@ -33,9 +33,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.geeksville.mesh.R import com.geeksville.mesh.model.BTScanModel +import com.geeksville.mesh.service.MeshService @Composable fun UsbDevices( + connectionState: MeshService.ConnectionState, usbDevices: List, selectedDevice: String, scanModel: BTScanModel @@ -45,13 +47,12 @@ fun UsbDevices( style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(vertical = 8.dp) ) - if (usbDevices.isNotEmpty()) { - usbDevices.forEach { device -> - DeviceListItem(device, device.fullAddress == selectedDevice) { - scanModel.onSelected(device) - } + usbDevices.forEach { device -> + DeviceListItem(connectionState, device, device.fullAddress == selectedDevice) { + scanModel.onSelected(device) } - } else { + } + if (usbDevices.filterNot { it.isDisconnect || it.isMock }.isEmpty()) { Column( modifier = Modifier .fillMaxWidth() 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 291b6958e..e4624da2e 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 @@ -952,7 +952,10 @@ fun TracerouteActionButton( @Suppress("LongMethod") @Composable fun NodeActionButton( - modifier: Modifier = Modifier, + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .height(48.dp), title: String, enabled: Boolean, icon: ImageVector? = null, @@ -966,9 +969,6 @@ fun NodeActionButton( }, enabled = enabled, modifier = modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .height(48.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt index e9ba7aedf..6042c08a7 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeStatusIcons.kt @@ -23,8 +23,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.NoCell import androidx.compose.material.icons.rounded.Star -import androidx.compose.material.icons.twotone.CloudOff import androidx.compose.material.icons.twotone.CloudDone +import androidx.compose.material.icons.twotone.CloudOff import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -150,6 +150,6 @@ fun StatusIconsPreview() { isThisNode = true, isUnmessageable = true, isFavorite = true, - isConnected = true, + isConnected = false, ) }