From be30757720f262adfe2d1b3d8cbc7381ddb51ca1 Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Mon, 21 Jul 2025 00:48:34 +1000 Subject: [PATCH] allow deleting of recent nodes, use long name (#2456) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../com/geeksville/mesh/model/BTScanModel.kt | 58 +++++++++++++++---- .../mesh/ui/connections/Connections.kt | 2 + .../ui/connections/components/BLEDevices.kt | 12 ++-- .../connections/components/DeviceListItem.kt | 18 ++++-- .../connections/components/NetworkDevices.kt | 46 ++++++++++++++- .../ui/connections/components/UsbDevices.kt | 10 +++- app/src/main/res/values/strings.xml | 3 +- 7 files changed, 119 insertions(+), 30 deletions(-) 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 7f72cd9a8..ff5cd77dc 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -127,8 +127,8 @@ class BTScanModel @Inject constructor( } // Include saved IP connections - recent.forEach { address -> - addDevice(DeviceListEntry(context.getString(R.string.meshtastic), address, true)) + recent.forEach { (address, name) -> + addDevice(DeviceListEntry(name, address, true)) } usb.forEach { (_, d) -> @@ -295,27 +295,58 @@ class BTScanModel @Inject constructor( }.launchIn(viewModelScope) } - private fun getRecentAddresses(): List { + private fun getRecentAddresses(): List> { val jsonAddresses = preferences.getString("recent-ip-addresses", "[]") ?: "[]" - val listAddresses = JSONArray(jsonAddresses).let { jsonArray -> - List(jsonArray.length()) { index -> jsonArray.getString(index) } + val jsonArray = JSONArray(jsonAddresses) + val listAddresses = mutableListOf>() + var needsMigration = false + for (i in 0 until jsonArray.length()) { + val item = jsonArray.get(i) + if (item is org.json.JSONObject) { + val address = item.getString("address") + val name = item.getString("name") + listAddresses.add(address to name) + } else if (item is String) { + // Old format: just the address, use default name and mark for migration + listAddresses.add(item to context.getString(R.string.meshtastic)) + needsMigration = true + } + } + // If migration is needed, rewrite the storage in the new format + if (needsMigration) { + setRecentAddresses(listAddresses) } return listAddresses } - private fun setRecentAddresses(addresses: List) { + private fun setRecentAddresses(addresses: List>) { + val jsonArray = JSONArray() + addresses.forEach { (address, name) -> + val obj = org.json.JSONObject() + obj.put("address", address) + obj.put("name", name) + jsonArray.put(obj) + } preferences.edit { - putString("recent-ip-addresses", addresses.toString()) + putString("recent-ip-addresses", jsonArray.toString()) } recentIpAddresses.value = addresses } - fun addRecentAddress(address: String) { + // Remove 'name' parameter from addRecentAddress and related logic + fun addRecentAddress(address: String, overrideName: String? = null) { if (!address.startsWith("t")) return val existingItems = getRecentAddresses() - val updatedList = mutableListOf() - updatedList.add(address) - updatedList.addAll(existingItems.filter { it != address }.take(2)) + val updatedList = mutableListOf>() + val displayName = overrideName ?: context.getString(R.string.meshtastic) + updatedList.add(address to displayName) + updatedList.addAll(existingItems.filter { it.first != address }.take(2)) + setRecentAddresses(updatedList) + } + + fun removeRecentAddress(address: String) { + val existingItems = getRecentAddresses() + val updatedList = existingItems.filter { it.first != address } setRecentAddresses(updatedList) } @@ -324,7 +355,7 @@ class BTScanModel @Inject constructor( fun onSelected(it: DeviceListEntry): Boolean { // If the device is paired, let user select it, otherwise start the pairing flow if (it.bonded) { - addRecentAddress(it.fullAddress) + addRecentAddress(it.fullAddress, connectedNodeLongName) changeDeviceAddress(it.fullAddress) return true } else { @@ -345,6 +376,9 @@ class BTScanModel @Inject constructor( private val _spinner = MutableStateFlow(false) val spinner: StateFlow get() = _spinner.asStateFlow() + + // Add a new property to hold the connected node's long name + var connectedNodeLongName: String? = null } const val NO_DEVICE_SELECTED = "n" 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 61651a3e6..f2fed9f9e 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 @@ -278,6 +278,8 @@ fun ConnectionsScreen( val isConnected by uiViewModel.isConnected.collectAsState(false) val ourNode by uiViewModel.ourNodeInfo.collectAsState() + // Set the connected node long name for BTScanModel + scanModel.connectedNodeLongName = ourNode?.user?.longName if (isConnected) { ourNode?.let { node -> Row( 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 7b13c3300..53c53a6ce 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 @@ -63,12 +63,12 @@ fun BLEDevices( ) btDevices.forEach { device -> DeviceListItem( - connectionState, - device, - device.fullAddress == selectedDevice - ) { - scanModel.onSelected(device) - } + connectionState = connectionState, + device = device, + selected = device.fullAddress == selectedDevice, + onSelect = { scanModel.onSelected(device) }, + modifier = Modifier + ) } if (isScanning) { Column( 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 a4e3cad8d..521b35743 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 @@ -48,6 +48,7 @@ fun DeviceListItem( device: BTScanModel.DeviceListEntry, selected: Boolean, onSelect: () -> Unit, + modifier: Modifier = Modifier, ) { val icon = if (device.isBLE) { Icons.Default.Bluetooth @@ -102,13 +103,18 @@ fun DeviceListItem( } } + val useSelectable = modifier == Modifier ListItem( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selected, - onClick = onSelect, - ), + modifier = if (useSelectable) { + modifier + .fillMaxWidth() + .selectable( + selected = selected, + onClick = onSelect, + ) + } else { + modifier.fillMaxWidth() + }, headlineContent = { Text(device.name) }, leadingContent = { Icon( 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 41dfc0914..a43afe00c 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 @@ -47,6 +47,13 @@ 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 +import androidx.compose.foundation.combinedClickable +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Suppress("MagicNumber", "LongMethod") @@ -59,15 +66,50 @@ fun NetworkDevices( ) { val manualIpAddress = rememberTextFieldState("") val manualIpPort = rememberTextFieldState(NetworkRepository.Companion.SERVICE_PORT.toString()) + var showDeleteDialog by remember { mutableStateOf(false) } + var deviceToDelete by remember { mutableStateOf(null) } 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) + val isRecent = device.isTCP && device.fullAddress.startsWith("t") + val modifier = if (isRecent) { + Modifier.combinedClickable( + onClick = { scanModel.onSelected(device) }, + onLongClick = { + deviceToDelete = device + showDeleteDialog = true + } + ) + } else { + Modifier } + DeviceListItem( + connectionState, device, device.fullAddress == selectedDevice, onSelect = { scanModel.onSelected(device) }, + modifier = modifier + ) + } + if (showDeleteDialog && deviceToDelete != null) { + AlertDialog( + onDismissRequest = { showDeleteDialog = false }, + title = { Text(stringResource(R.string.delete)) }, + text = { Text(stringResource(R.string.confirm_delete_node)) }, + confirmButton = { + Button(onClick = { + scanModel.removeRecentAddress(deviceToDelete!!.fullAddress) + showDeleteDialog = false + }) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + Button(onClick = { showDeleteDialog = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) } if (networkDevices.filterNot { it.isDisconnect }.isEmpty()) { Column( 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 a1703eb2e..412d9c72b 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 @@ -48,9 +48,13 @@ fun UsbDevices( modifier = Modifier.padding(vertical = 8.dp) ) usbDevices.forEach { device -> - DeviceListItem(connectionState, device, device.fullAddress == selectedDevice) { - scanModel.onSelected(device) - } + DeviceListItem( + connectionState = connectionState, + device = device, + selected = device.fullAddress == selectedDevice, + onSelect = { scanModel.onSelected(device) }, + modifier = Modifier + ) } if (usbDevices.filterNot { it.isDisconnect || it.isMock }.isEmpty()) { Column( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fdab6f061..7685727e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -780,6 +780,8 @@ Show Current Status Dismiss + Are you sure you want to delete this node? + Replying to %1$s Cancel reply Delete Messages? @@ -787,5 +789,4 @@ Message Type a message -