mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-06 13:53:02 -05:00
Refactor: Improve connection state display in UI (#2353)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -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<String>) {
|
||||
val editor = preferences.edit()
|
||||
editor.putString("recent-ip-addresses", addresses.toString())
|
||||
editor.apply()
|
||||
preferences.edit {
|
||||
putString("recent-ip-addresses", addresses.toString())
|
||||
}
|
||||
recentIpAddresses.value = addresses
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<BTScanModel.DeviceListEntry>,
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<BTScanModel.DeviceListEntry>,
|
||||
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,
|
||||
|
||||
@@ -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<BTScanModel.DeviceListEntry>,
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user