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>
This commit is contained in:
DaneEvans 2025-07-21 00:48:34 +10:00 committed by GitHub
parent 5e5fc19fc0
commit be30757720
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 119 additions and 30 deletions

View file

@ -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<String> {
private fun getRecentAddresses(): List<Pair<String, String>> {
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<Pair<String, String>>()
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<String>) {
private fun setRecentAddresses(addresses: List<Pair<String, String>>) {
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<String>()
updatedList.add(address)
updatedList.addAll(existingItems.filter { it != address }.take(2))
val updatedList = mutableListOf<Pair<String, String>>()
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<Boolean> 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"

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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<BTScanModel.DeviceListEntry?>(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(

View file

@ -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(