mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Refactor Connections screen and add new strings (#2236)
This commit is contained in:
parent
ee03213a3a
commit
8d32638902
7 changed files with 802 additions and 378 deletions
|
|
@ -42,6 +42,7 @@ import com.hoho.android.usbserial.driver.UsbSerialDriver
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
|
@ -49,7 +50,6 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -127,6 +127,9 @@ class BTScanModel @Inject constructor(
|
|||
val isBLE: Boolean get() = prefix == 'x'
|
||||
val isUSB: Boolean get() = prefix == 's'
|
||||
val isTCP: Boolean get() = prefix == 't'
|
||||
|
||||
val isMock: Boolean get() = prefix == 'm'
|
||||
val isDisconnect: Boolean get() = prefix == 'n'
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
|
|
|
|||
|
|
@ -39,21 +39,25 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.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.FloatingActionButton
|
||||
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.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
|
|
@ -70,7 +74,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
|
@ -92,15 +95,16 @@ import com.geeksville.mesh.android.isGooglePlayAvailable
|
|||
import com.geeksville.mesh.android.permissionMissing
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.ConfigRoute
|
||||
import com.geeksville.mesh.navigation.RadioConfigRoutes
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.ui.connections.components.BLEDevices
|
||||
import com.geeksville.mesh.ui.connections.components.NetworkDevices
|
||||
import com.geeksville.mesh.ui.connections.components.UsbDevices
|
||||
import com.geeksville.mesh.ui.node.NodeActionButton
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
|
|
@ -186,10 +190,6 @@ fun ConnectionsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// State for manual IP address input
|
||||
var manualIpAddress by remember { mutableStateOf("") }
|
||||
var manualIpPort by remember { mutableStateOf(NetworkRepository.SERVICE_PORT.toString()) }
|
||||
|
||||
// State for the device scan dialog
|
||||
var showScanDialog by remember { mutableStateOf(false) }
|
||||
val scanResults by scanModel.scanResult.observeAsState(emptyMap())
|
||||
|
|
@ -267,11 +267,9 @@ fun ConnectionsScreen(
|
|||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
// Scan Status Text
|
||||
Text(
|
||||
text = scanStatusText.orEmpty(),
|
||||
fontSize = 14.sp,
|
||||
|
|
@ -286,6 +284,7 @@ fun ConnectionsScreen(
|
|||
if (isConnected) {
|
||||
ourNode?.let { node ->
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NodeChip(
|
||||
|
|
@ -308,17 +307,37 @@ fun ConnectionsScreen(
|
|||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f, fill = true),
|
||||
text = node.user.longName,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
IconButton(
|
||||
enabled = true,
|
||||
onClick = onNavigateToRadioConfig
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeActionButton(
|
||||
title = stringResource(id = R.string.radio_configuration),
|
||||
icon = Icons.Default.Settings,
|
||||
enabled = true,
|
||||
onClick = onNavigateToRadioConfig
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (regionUnset && selectedDevice != "m") {
|
||||
NodeActionButton(
|
||||
|
|
@ -331,394 +350,354 @@ fun ConnectionsScreen(
|
|||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (scanning) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device List and Manual Input
|
||||
Text(
|
||||
text = stringResource(R.string.device),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
// Progress bar while scanning
|
||||
if (scanning) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) }
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
DeviceType.BLE.ordinal,
|
||||
DeviceType.entries.size
|
||||
),
|
||||
onClick = {
|
||||
selectedDeviceType = DeviceType.BLE
|
||||
},
|
||||
selected = (selectedDeviceType == DeviceType.BLE),
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = stringResource(id = R.string.bluetooth)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.bluetooth))
|
||||
}
|
||||
)
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
DeviceType.TCP.ordinal,
|
||||
DeviceType.entries.size
|
||||
),
|
||||
onClick = {
|
||||
selectedDeviceType = DeviceType.TCP
|
||||
},
|
||||
selected = (selectedDeviceType == DeviceType.TCP),
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Wifi,
|
||||
contentDescription = stringResource(id = R.string.network)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.network))
|
||||
}
|
||||
)
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
DeviceType.USB.ordinal,
|
||||
DeviceType.entries.size
|
||||
),
|
||||
onClick = {
|
||||
selectedDeviceType = DeviceType.USB
|
||||
},
|
||||
selected = (selectedDeviceType == DeviceType.USB),
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Usb,
|
||||
contentDescription = stringResource(id = R.string.serial)
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.serial))
|
||||
}
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
devices.values.forEach { device ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
when (selectedDeviceType) {
|
||||
DeviceType.BLE -> {
|
||||
|
||||
BLEDevices(
|
||||
devices.values.filter { it.isBLE },
|
||||
selectedDevice,
|
||||
showBluetoothRationaleDialog = {
|
||||
showBluetoothRationaleDialog = true
|
||||
},
|
||||
requestBluetoothPermission = {
|
||||
requestBluetoothPermissionLauncher.launch(
|
||||
it
|
||||
)
|
||||
},
|
||||
scanModel
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.TCP -> {
|
||||
NetworkDevices(
|
||||
devices.values.filter { it.isTCP },
|
||||
selectedDevice,
|
||||
scanModel
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.USB -> {
|
||||
UsbDevices(
|
||||
devices.values.filter { it.isUSB || it.isMock },
|
||||
selectedDevice,
|
||||
scanModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
LaunchedEffect(ourNode) {
|
||||
if (ourNode != null) {
|
||||
uiViewModel.refreshProvideLocation()
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(isConnected) {
|
||||
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
|
||||
LaunchedEffect(provideLocation) {
|
||||
if (provideLocation) {
|
||||
if (!context.hasLocationPermission()) {
|
||||
debug("Requesting location permission for providing location")
|
||||
showLocationRationaleDialog = true
|
||||
} else if (isGpsDisabled) {
|
||||
uiViewModel.showSnackbar(context.getString(R.string.location_disabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = (device.fullAddress == selectedDevice) ||
|
||||
device.fullAddress == NO_DEVICE_SELECTED,
|
||||
onClick = {
|
||||
if (!device.bonded) {
|
||||
uiViewModel.showSnackbar(context.getString(R.string.starting_pairing))
|
||||
}
|
||||
scanModel.onSelected(device)
|
||||
.toggleable(
|
||||
value = provideLocation,
|
||||
onValueChange = { checked ->
|
||||
uiViewModel.setProvideLocation(checked)
|
||||
},
|
||||
role = Role.RadioButton,
|
||||
enabled = !isGpsDisabled
|
||||
)
|
||||
.padding(8.dp),
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (device.fullAddress == selectedDevice),
|
||||
onClick = null
|
||||
Checkbox(
|
||||
checked = receivingLocationUpdates,
|
||||
onCheckedChange = null,
|
||||
enabled = !isGpsDisabled // Disable if GPS is disabled
|
||||
)
|
||||
Text(
|
||||
text = device.name,
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Provide Location Checkbox
|
||||
|
||||
// Manual IP Address Input
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice),
|
||||
onClick = {
|
||||
if (manualIpAddress.isIPAddress()) {
|
||||
scanModel.onSelected(
|
||||
BTScanModel.DeviceListEntry(
|
||||
"",
|
||||
"t$manualIpAddress:$manualIpPort",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = manualIpAddress.isIPAddress(),
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice),
|
||||
onClick = null,
|
||||
enabled = manualIpAddress.isIPAddress()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = manualIpAddress,
|
||||
onValueChange = { manualIpAddress = it },
|
||||
label = { Text(stringResource(R.string.ip_address)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
if (manualIpAddress.isIPAddress()) {
|
||||
scanModel.onSelected(
|
||||
BTScanModel.DeviceListEntry(
|
||||
"",
|
||||
"t$manualIpAddress:$manualIpPort",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(0.7f)
|
||||
.padding(start = 16.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = manualIpPort,
|
||||
onValueChange = {
|
||||
// Only allow numeric input for port
|
||||
if (it.all { char -> char.isDigit() }) {
|
||||
manualIpPort = it
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.ip_port)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = androidx.compose.ui.text.input.ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
if (manualIpAddress.isIPAddress()) {
|
||||
scanModel.onSelected(
|
||||
BTScanModel.DeviceListEntry(
|
||||
"",
|
||||
"t$manualIpAddress:$manualIpPort",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(weight = 0.3f)
|
||||
.padding(start = 8.dp)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning Not Paired
|
||||
val showWarningNotPaired = !devices.any { it.value.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))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Analytics Okay Checkbox
|
||||
|
||||
LaunchedEffect(ourNode) {
|
||||
if (ourNode != null) {
|
||||
uiViewModel.refreshProvideLocation()
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(isConnected) {
|
||||
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
|
||||
LaunchedEffect(provideLocation) {
|
||||
if (provideLocation) {
|
||||
if (!context.hasLocationPermission()) {
|
||||
debug("Requesting location permission for providing location")
|
||||
showLocationRationaleDialog = true
|
||||
} else if (isGpsDisabled) {
|
||||
uiViewModel.showSnackbar(context.getString(R.string.location_disabled))
|
||||
}
|
||||
val isGooglePlayAvailable = app.isGooglePlayAvailable()
|
||||
val isAnalyticsAllowed = app.isAnalyticsAllowed && isGooglePlayAvailable
|
||||
if (isGooglePlayAvailable) {
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(isAnalyticsAllowed) {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = provideLocation,
|
||||
onValueChange = { checked ->
|
||||
uiViewModel.setProvideLocation(checked)
|
||||
},
|
||||
enabled = !isGpsDisabled
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = receivingLocationUpdates,
|
||||
onCheckedChange = null,
|
||||
enabled = !isGpsDisabled // Disable if GPS is disabled
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Provide Location Checkbox
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning Not Paired
|
||||
val showWarningNotPaired = !devices.any { it.value.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))
|
||||
}
|
||||
|
||||
// Analytics Okay Checkbox
|
||||
|
||||
val isGooglePlayAvailable = app.isGooglePlayAvailable()
|
||||
val isAnalyticsAllowed = app.isAnalyticsAllowed && isGooglePlayAvailable
|
||||
if (isGooglePlayAvailable) {
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(isAnalyticsAllowed) {
|
||||
loading = false
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = isAnalyticsAllowed,
|
||||
onValueChange = {
|
||||
debug("User changed analytics to $it")
|
||||
app.isAnalyticsAllowed = it
|
||||
loading = true
|
||||
},
|
||||
role = Role.Checkbox,
|
||||
enabled = isGooglePlayAvailable && !loading
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
enabled = isGooglePlayAvailable,
|
||||
checked = isAnalyticsAllowed,
|
||||
onCheckedChange = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Report Bug Button
|
||||
Button(
|
||||
onClick = { showReportBugDialog = true }, // Set state to show Report Bug dialog
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
enabled = isAnalyticsAllowed
|
||||
) {
|
||||
Text(stringResource(R.string.report_bug))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Floating Action Button (Change Radio)
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
val bluetoothPermissions = context.getBluetoothPermissions()
|
||||
if (bluetoothPermissions.isEmpty()) {
|
||||
// If no permissions needed, trigger the scan directly (or via ViewModel)
|
||||
scanModel.startScan()
|
||||
} else {
|
||||
if (
|
||||
context.findActivity()
|
||||
.shouldShowRequestPermissionRationale(bluetoothPermissions.first())
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = isAnalyticsAllowed,
|
||||
onValueChange = {
|
||||
debug("User changed analytics to $it")
|
||||
app.isAnalyticsAllowed = it
|
||||
loading = true
|
||||
},
|
||||
role = Role.Checkbox,
|
||||
enabled = isGooglePlayAvailable && !loading
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
showBluetoothRationaleDialog = true
|
||||
} else {
|
||||
requestBluetoothPermissionLauncher.launch(bluetoothPermissions)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.change_radio))
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Checkbox(
|
||||
enabled = isGooglePlayAvailable,
|
||||
checked = isAnalyticsAllowed,
|
||||
onCheckedChange = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(onClick = {
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
// Report Bug Button
|
||||
Button(
|
||||
onClick = {
|
||||
showReportBugDialog = true
|
||||
}, // Set state to show Report Bug dialog
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
enabled = isAnalyticsAllowed
|
||||
) {
|
||||
Text(stringResource(R.string.report_bug))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Location Permission Rationale Dialog
|
||||
if (showLocationRationaleDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLocationRationaleDialog = false },
|
||||
title = { Text(stringResource(R.string.background_required)) },
|
||||
text = { Text(stringResource(R.string.why_background_required)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showLocationRationaleDialog = false
|
||||
if (!context.hasLocationPermission()) {
|
||||
requestLocationPermissionLauncher.launch(context.getLocationPermissions())
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showLocationRationaleDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Bluetooth Permission Rationale Dialog
|
||||
if (showBluetoothRationaleDialog) {
|
||||
val bluetoothPermissions = context.getBluetoothPermissions()
|
||||
AlertDialog(
|
||||
onDismissRequest = { showBluetoothRationaleDialog = false },
|
||||
title = { Text(stringResource(R.string.required_permissions)) },
|
||||
text = { Text(stringResource(R.string.permission_missing_31)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showBluetoothRationaleDialog = false
|
||||
if (bluetoothPermissions.isNotEmpty()) {
|
||||
requestBluetoothPermissionLauncher.launch(bluetoothPermissions)
|
||||
} else {
|
||||
// If somehow no permissions are required, just scan
|
||||
scanModel.startScan()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showBluetoothRationaleDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Report Bug Dialog
|
||||
if (showReportBugDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showReportBugDialog = false },
|
||||
title = { Text(stringResource(R.string.report_a_bug)) },
|
||||
text = { Text(stringResource(R.string.report_bug_text)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showReportBugDialog = false
|
||||
reportError("Clicked Report A Bug")
|
||||
uiViewModel.showSnackbar("Bug report sent!")
|
||||
}) {
|
||||
Text(stringResource(R.string.report))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = {
|
||||
showReportBugDialog = false
|
||||
debug("Decided not to report a bug")
|
||||
}) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Location Permission Rationale Dialog
|
||||
if (showLocationRationaleDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLocationRationaleDialog = false },
|
||||
title = { Text(stringResource(R.string.background_required)) },
|
||||
text = { Text(stringResource(R.string.why_background_required)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showLocationRationaleDialog = false
|
||||
if (!context.hasLocationPermission()) {
|
||||
requestLocationPermissionLauncher.launch(context.getLocationPermissions())
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showLocationRationaleDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Bluetooth Permission Rationale Dialog
|
||||
if (showBluetoothRationaleDialog) {
|
||||
val bluetoothPermissions = context.getBluetoothPermissions()
|
||||
AlertDialog(
|
||||
onDismissRequest = { showBluetoothRationaleDialog = false },
|
||||
title = { Text(stringResource(R.string.required_permissions)) },
|
||||
text = { Text(stringResource(R.string.permission_missing_31)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showBluetoothRationaleDialog = false
|
||||
if (bluetoothPermissions.isNotEmpty()) {
|
||||
requestBluetoothPermissionLauncher.launch(bluetoothPermissions)
|
||||
} else {
|
||||
// If somehow no permissions are required, just scan
|
||||
scanModel.startScan()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.okay))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { showBluetoothRationaleDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Report Bug Dialog
|
||||
if (showReportBugDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showReportBugDialog = false },
|
||||
title = { Text(stringResource(R.string.report_a_bug)) },
|
||||
text = { Text(stringResource(R.string.report_bug_text)) },
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
showReportBugDialog = false
|
||||
reportError("Clicked Report A Bug")
|
||||
uiViewModel.showSnackbar("Bug report sent!")
|
||||
}) {
|
||||
Text(stringResource(R.string.report))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = {
|
||||
showReportBugDialog = false
|
||||
debug("Decided not to report a bug")
|
||||
}) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun Context.findActivity(): Activity = when (this) {
|
||||
|
|
@ -727,4 +706,10 @@ private tailrec fun Context.findActivity(): Activity = when (this) {
|
|||
else -> error("No activity found")
|
||||
}
|
||||
|
||||
private enum class DeviceType {
|
||||
BLE,
|
||||
TCP,
|
||||
USB,
|
||||
}
|
||||
|
||||
private const val SCAN_PERIOD: Long = 10000 // 10 seconds
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.BluetoothDisabled
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.getBluetoothPermissions
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BLEDevices(
|
||||
btDevices: List<BTScanModel.DeviceListEntry>,
|
||||
selectedDevice: String,
|
||||
showBluetoothRationaleDialog: () -> Unit,
|
||||
requestBluetoothPermission: (Array<String>) -> Unit,
|
||||
scanModel: BTScanModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BluetoothDisabled,
|
||||
contentDescription = stringResource(R.string.no_ble_devices),
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_ble_devices),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
val bluetoothPermissions = context.getBluetoothPermissions()
|
||||
if (bluetoothPermissions.isEmpty()) {
|
||||
// If no permissions needed, trigger the scan directly (or via ViewModel)
|
||||
scanModel.startScan()
|
||||
} else {
|
||||
if (bluetoothPermissions.any { permission ->
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
context as Activity,
|
||||
permission
|
||||
)
|
||||
}
|
||||
) {
|
||||
showBluetoothRationaleDialog()
|
||||
} else {
|
||||
requestBluetoothPermission(bluetoothPermissions)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = stringResource(R.string.scan)
|
||||
)
|
||||
Text(stringResource(R.string.scan))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
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.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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
|
||||
@Composable
|
||||
fun DeviceListItem(
|
||||
device: BTScanModel.DeviceListEntry,
|
||||
selected: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
) {
|
||||
val icon = if (device.isBLE) {
|
||||
Icons.Default.Bluetooth
|
||||
} else if (device.isUSB) {
|
||||
Icons.Default.Usb
|
||||
} else if (device.isTCP) {
|
||||
Icons.Default.Wifi
|
||||
} else if (device.isDisconnect) {
|
||||
Icons.Default.Cancel
|
||||
} else {
|
||||
Icons.Default.Add
|
||||
}
|
||||
|
||||
val contentDescription = if (device.isBLE) {
|
||||
stringResource(R.string.bluetooth)
|
||||
} else if (device.isUSB) {
|
||||
stringResource(R.string.serial)
|
||||
} else if (device.isTCP) {
|
||||
stringResource(R.string.network)
|
||||
} else if (device.isDisconnect) {
|
||||
stringResource(R.string.disconnect)
|
||||
} else {
|
||||
stringResource(R.string.add)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = onSelect,
|
||||
),
|
||||
headlineContent = { Text(device.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (selected) {
|
||||
Icon(
|
||||
Icons.Outlined.CloudDone,
|
||||
stringResource(R.string.connected),
|
||||
tint = Color(color = 0xFF30C047)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Default.CloudQueue,
|
||||
stringResource(R.string.not_connected)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.WifiFind
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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.ui.connections.isIPAddress
|
||||
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
fun NetworkDevices(
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.WifiFind,
|
||||
contentDescription = stringResource(R.string.no_network_devices),
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_network_devices),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice),
|
||||
onClick = {
|
||||
if (manualIpAddress.text.toString().isIPAddress()) {
|
||||
scanModel.onSelected(
|
||||
BTScanModel.DeviceListEntry(
|
||||
"",
|
||||
"t$manualIpAddress:$manualIpPort",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = manualIpAddress.text.toString().isIPAddress(),
|
||||
role = Role.Companion.RadioButton
|
||||
)
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.Companion.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice),
|
||||
onClick = null,
|
||||
enabled = manualIpAddress.toString().isIPAddress()
|
||||
)
|
||||
OutlinedTextField(
|
||||
state = manualIpAddress,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
label = { Text(stringResource(R.string.ip_address)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Companion.Number,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(0.7f)
|
||||
.padding(start = 16.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
state = manualIpPort,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
label = { Text(stringResource(R.string.ip_port)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Companion.Number,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(weight = 0.3f)
|
||||
.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.UsbOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
|
||||
@Composable
|
||||
fun UsbDevices(
|
||||
usbDevices: List<BTScanModel.DeviceListEntry>,
|
||||
selectedDevice: String,
|
||||
scanModel: BTScanModel
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.serial),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
if (usbDevices.isNotEmpty()) {
|
||||
usbDevices.forEach { device ->
|
||||
DeviceListItem(device, device.fullAddress == selectedDevice) {
|
||||
scanModel.onSelected(device)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalAlignment = CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.UsbOff,
|
||||
contentDescription = stringResource(R.string.no_usb_devices),
|
||||
modifier = Modifier.size(96.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_usb_devices),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -697,4 +697,8 @@
|
|||
<string name="remote">Remote</string>
|
||||
<string name="node_count_template">(%1$d online / %2$d total)</string>
|
||||
<string name="react">React</string>
|
||||
<string name="disconnect">Disconnect</string>
|
||||
<string name="no_ble_devices">No Bluetooth devices found.</string>
|
||||
<string name="no_network_devices">No Network devices found.</string>
|
||||
<string name="no_usb_devices">No USB Serial devices found.</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue