feat(settings): align config screens copy and order with iOS (#3144)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-09-19 17:58:49 -05:00 committed by GitHub
parent 8fb41aab74
commit 00ee0db78a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 899 additions and 724 deletions

View file

@ -219,6 +219,7 @@ private fun NavGraphBuilder.configRoutesScreens(navController: NavHostController
entry.name,
entry.screenComposable,
)
else -> Unit // Should not happen if ConfigRoute enum is exhaustive for this context
}
}
@ -374,7 +375,7 @@ enum class ConfigRoute(
;
companion object {
fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
private fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = entries.filter {
when {
metadata == null -> true // Include all routes if metadata is null
it == BLUETOOTH -> metadata.hasBluetooth
@ -382,6 +383,11 @@ enum class ConfigRoute(
else -> true // Include all other routes by default
}
}
val radioConfigRoutes = listOf(LORA, CHANNELS, SECURITY)
fun deviceConfigRoutes(metadata: DeviceMetadata?): List<ConfigRoute> =
filterExcludedFrom(metadata) - radioConfigRoutes
}
}

View file

@ -17,15 +17,16 @@
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.KeyboardArrowDown
import androidx.compose.material.icons.twotone.KeyboardArrowUp
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -36,8 +37,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BitwisePreference(
title: String,
@ -46,44 +49,43 @@ fun BitwisePreference(
items: List<Pair<Int, String>>,
onItemSelected: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
var dropDownExpanded by remember { mutableStateOf(value = false) }
var expanded by remember { mutableStateOf(false) }
RegularPreference(
title = title,
subtitle = value.toString(),
onClick = { dropDownExpanded = !dropDownExpanded },
enabled = enabled,
trailingIcon = if (dropDownExpanded) {
Icons.TwoTone.KeyboardArrowUp
} else {
Icons.TwoTone.KeyboardArrowDown
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
)
Box {
DropdownMenu(
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = !dropDownExpanded },
) {
modifier = modifier.padding(vertical = 8.dp),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = value.toString(),
onValueChange = {},
label = { Text(title) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
enabled = enabled,
supportingText = { if (summary != null) Text(text = summary) },
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items.forEach { item ->
DropdownMenuItem(
onClick = { onItemSelected(value xor item.first) },
modifier = modifier.fillMaxWidth(),
text = {
Text(
text = item.second,
overflow = TextOverflow.Ellipsis,
)
Text(text = item.second, overflow = TextOverflow.Ellipsis)
Checkbox(
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
checked = value and item.first != 0,
onCheckedChange = { onItemSelected(value xor item.first) },
enabled = enabled,
)
}
},
onClick = { onItemSelected(value xor item.first) },
)
}
PreferenceFooter(
@ -91,7 +93,7 @@ fun BitwisePreference(
negativeText = R.string.clear,
onNegativeClicked = { onItemSelected(0) },
positiveText = R.string.close,
onPositiveClicked = { dropDownExpanded = false },
onPositiveClicked = { expanded = false },
)
}
}
@ -103,8 +105,9 @@ private fun BitwisePreferencePreview() {
BitwisePreference(
title = "Settings",
value = 3,
summary = "This is a summary",
enabled = true,
items = listOf(1 to "TEST1", 2 to "TEST2"),
onItemSelected = {}
onItemSelected = {},
)
}

View file

@ -17,13 +17,14 @@
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.background
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.KeyboardArrowDown
import androidx.compose.material.icons.twotone.KeyboardArrowUp
import androidx.compose.material3.DropdownMenu
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -31,11 +32,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.R
import androidx.compose.ui.unit.dp
import com.google.protobuf.ProtocolMessageEnum
@Composable
@ -50,8 +48,9 @@ fun <T : Enum<T>> DropDownPreference(
DropDownPreference(
title = title,
enabled = enabled,
items = selectedItem.declaringJavaClass.enumConstants
?.filter { it.name != "UNRECOGNIZED" }?.map { it to it.name } ?: emptyList(),
items =
selectedItem.declaringJavaClass.enumConstants?.filter { it.name != "UNRECOGNIZED" }?.map { it to it.name }
?: emptyList(),
selectedItem = selectedItem,
onItemSelected = onItemSelected,
modifier = modifier,
@ -59,6 +58,7 @@ fun <T : Enum<T>> DropDownPreference(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DropDownPreference(
title: String,
@ -69,7 +69,7 @@ fun <T> DropDownPreference(
modifier: Modifier = Modifier,
summary: String? = null,
) {
var dropDownExpanded by remember { mutableStateOf(value = false) }
var expanded by remember { mutableStateOf(false) }
val deprecatedItems: List<T> = remember {
if (selectedItem is ProtocolMessageEnum) {
@ -77,58 +77,46 @@ fun <T> DropDownPreference(
val descriptor = (selectedItem as ProtocolMessageEnum).descriptorForType
@Suppress("UNCHECKED_CAST")
enum?.filter { entries ->
descriptor.values.any { it.name == entries.name && it.options.deprecated }
} as? List<T> ?: emptyList() // Safe cast to List<T> or return emptyList if cast fails
enum?.filter { entries -> descriptor.values.any { it.name == entries.name && it.options.deprecated } }
as? List<T> ?: emptyList() // Safe cast to List<T> or return emptyList if cast fails
} else {
emptyList()
}
}
RegularPreference(
title = title,
subtitle = items.find { it.first == selectedItem }?.second
?: stringResource(id = R.string.unrecognized),
onClick = {
dropDownExpanded = true
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
enabled = enabled,
trailingIcon = if (dropDownExpanded) {
Icons.TwoTone.KeyboardArrowUp
} else {
Icons.TwoTone.KeyboardArrowDown
},
summary = summary,
dropdownMenu = {
DropdownMenu(
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = !dropDownExpanded },
) {
items.filterNot { it.first in deprecatedItems }.forEach { item ->
modifier = modifier.padding(vertical = 8.dp),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
onValueChange = {},
label = { Text(title) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
enabled = enabled,
supportingText = { if (summary != null) Text(text = summary) },
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
dropDownExpanded = false
onItemSelected(item.first)
onItemSelected(selectionOption.first)
expanded = false
},
modifier = modifier
.background(
color = if (selectedItem == item.first) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
} else {
Color.Unspecified
},
),
text = {
Text(
text = item.second,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
)
}
}
}
)
}
}
@Preview(showBackground = true)
@ -140,6 +128,6 @@ private fun DropDownPreferencePreview() {
enabled = true,
items = listOf("TEST1" to "text1", "TEST2" to "text2"),
selectedItem = "TEST2",
onItemSelected = {}
onItemSelected = {},
)
}

View file

@ -17,6 +17,7 @@
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
@ -54,6 +55,7 @@ import org.meshtastic.core.model.Channel
fun EditBase64Preference(
modifier: Modifier = Modifier,
title: String,
summary: String? = null,
value: ByteString,
enabled: Boolean,
readOnly: Boolean = false,
@ -79,50 +81,59 @@ fun EditBase64Preference(
onGenerateKey != null && !isFocused -> Icons.TwoTone.Refresh to stringResource(R.string.reset)
else -> null to null
}
OutlinedTextField(
value = valueState,
onValueChange = {
valueState = it
runCatching { it.toByteString() }.onSuccess(onValueChange)
},
modifier = modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused },
enabled = enabled,
readOnly = readOnly,
label = { Text(text = title) },
isError = isError,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
trailingIcon = {
if (icon != null) {
IconButton(
onClick = {
if (isError) {
valueState = value.encodeToString()
onValueChange(value)
} else if (onGenerateKey != null && !isFocused) {
onGenerateKey()
}
},
enabled = enabled,
) {
Icon(
imageVector = icon,
contentDescription = description,
tint =
if (isError) {
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
OutlinedTextField(
value = valueState,
onValueChange = {
valueState = it
runCatching { it.toByteString() }.onSuccess(onValueChange)
},
modifier = Modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused },
enabled = enabled,
readOnly = readOnly,
label = { Text(text = title) },
isError = isError,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
trailingIcon = {
if (icon != null) {
IconButton(
onClick = {
if (isError) {
valueState = value.encodeToString()
onValueChange(value)
} else if (onGenerateKey != null && !isFocused) {
onGenerateKey()
}
},
)
enabled = enabled,
) {
Icon(
imageVector = icon,
contentDescription = description,
tint =
if (isError) {
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
},
)
}
} else if (trailingIcon != null) {
trailingIcon()
}
} else if (trailingIcon != null) {
trailingIcon()
}
},
)
},
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
}
}
@Preview(showBackground = true)
@ -130,6 +141,7 @@ fun EditBase64Preference(
private fun EditBase64PreferencePreview() {
EditBase64Preference(
title = "Title",
summary = "This is a summary",
value = Channel.getRandomKey(),
enabled = true,
keyboardActions = KeyboardActions {},

View file

@ -47,6 +47,7 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.remoteHardwarePin
import com.google.protobuf.ByteString
@Suppress("LongMethod")
@Composable
inline fun <reified T> EditListPreference(
title: String,
@ -56,12 +57,21 @@ inline fun <reified T> EditListPreference(
keyboardActions: KeyboardActions,
crossinline onValuesChanged: (List<T>) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
val focusManager = LocalFocusManager.current
val listState = remember(list) { mutableStateListOf<T>().apply { addAll(list) } }
Column(modifier = modifier) {
Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium)
Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Text(text = title, style = MaterialTheme.typography.titleLarge)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
listState.forEachIndexed { index, value ->
val trailingIcon =
@Composable {
@ -80,80 +90,75 @@ inline fun <reified T> EditListPreference(
}
}
// handle lora.ignoreIncoming: List<Int>
if (value is Int) {
EditTextPreference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = it as T
onValuesChanged(listState)
},
modifier = modifier.fillMaxWidth(),
trailingIcon = trailingIcon,
)
}
// handle security.adminKey: List<ByteString>
if (value is ByteString) {
EditBase64Preference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChange = {
listState[index] = it as T
onValuesChanged(listState)
},
modifier = modifier.fillMaxWidth(),
trailingIcon = trailingIcon,
)
}
// handle remoteHardware.availablePins: List<RemoteHardwarePin>
if (value is RemoteHardwarePin) {
EditTextPreference(
title = stringResource(R.string.gpio_pin),
value = value.gpioPin,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
if (it in 0..255) {
listState[index] = value.copy { gpioPin = it } as T
when (value) {
is Int -> {
EditTextPreference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = it as T
onValuesChanged(listState)
}
},
)
EditTextPreference(
title = stringResource(R.string.name),
value = value.name,
maxSize = 14, // name max_size:15
enabled = enabled,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = value.copy { name = it } as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
DropDownPreference(
title = stringResource(R.string.type),
enabled = enabled,
items =
RemoteHardwarePinType.entries
.filter { it != RemoteHardwarePinType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = value.type,
onItemSelected = {
listState[index] = value.copy { type = it } as T
onValuesChanged(listState)
},
)
},
trailingIcon = trailingIcon,
)
}
is ByteString -> {
EditBase64Preference(
title = "${index + 1}/$maxCount",
value = value,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChange = {
listState[index] = it as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
}
is RemoteHardwarePin -> {
EditTextPreference(
title = stringResource(R.string.gpio_pin),
value = value.gpioPin,
enabled = enabled,
keyboardActions = keyboardActions,
onValueChanged = {
if (it in 0..255) {
listState[index] = value.copy { gpioPin = it } as T
onValuesChanged(listState)
}
},
)
EditTextPreference(
title = stringResource(R.string.name),
value = value.name,
maxSize = 14, // name max_size:15
enabled = enabled,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
listState[index] = value.copy { name = it } as T
onValuesChanged(listState)
},
trailingIcon = trailingIcon,
)
DropDownPreference(
title = stringResource(R.string.type),
enabled = enabled,
items =
RemoteHardwarePinType.entries
.filter { it != RemoteHardwarePinType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = value.type,
onItemSelected = {
listState[index] = value.copy { type = it } as T
onValuesChanged(listState)
},
)
}
}
}
OutlinedButton(
@ -182,6 +187,7 @@ private fun EditListPreferencePreview() {
Column {
EditListPreference(
title = stringResource(R.string.ignore_incoming),
summary = "This is a summary",
list = listOf(12345, 67890),
maxCount = 4,
enabled = true,

View file

@ -27,8 +27,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -54,6 +54,7 @@ fun SignedIntegerEditTextPreference(
keyboardActions: KeyboardActions,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
) {
@ -63,20 +64,17 @@ fun SignedIntegerEditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = valueState.toIntOrNull() == null,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
valueState = it
it.toIntOrNull()?.let { int ->
onValueChanged(int)
}
it.toIntOrNull()?.let { int -> onValueChanged(int) }
},
onFocusChanged = onFocusChanged,
modifier = modifier,
trailingIcon = trailingIcon
trailingIcon = trailingIcon,
)
}
@ -89,6 +87,7 @@ fun EditTextPreference(
keyboardActions: KeyboardActions,
onValueChanged: (Int) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
) {
@ -98,21 +97,23 @@ fun EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toUInt().toString() != valueState || isError,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.isEmpty()) valueState = it
else it.toUIntOrNull()?.toInt()?.let { int ->
if (it.isEmpty()) {
valueState = it
onValueChanged(int)
} else {
it.toUIntOrNull()?.toInt()?.let { int ->
valueState = it
onValueChanged(int)
}
}
},
onFocusChanged = onFocusChanged,
modifier = modifier,
trailingIcon = trailingIcon
trailingIcon = trailingIcon,
)
}
@ -124,28 +125,31 @@ fun EditTextPreference(
keyboardActions: KeyboardActions,
onValueChanged: (Float) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
onFocusChanged: (FocusState) -> Unit = {},
) {
) {
var valueState by remember(value) { mutableStateOf(value.toString()) }
EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toString() != valueState,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.isEmpty()) valueState = it
else it.toFloatOrNull()?.let { float ->
if (it.isEmpty()) {
valueState = it
onValueChanged(float)
} else {
it.toFloatOrNull()?.let { float ->
valueState = it
onValueChanged(float)
}
}
},
onFocusChanged = onFocusChanged,
modifier = modifier
modifier = modifier,
)
}
@ -157,6 +161,7 @@ fun EditTextPreference(
keyboardActions: KeyboardActions,
onValueChanged: (Double) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
var valueState by remember(value) { mutableStateOf(value.toString()) }
val decimalSeparators = setOf('.', ',', '٫', '、', '·') // set of possible decimal separators
@ -165,20 +170,22 @@ fun EditTextPreference(
title = title,
value = valueState,
enabled = enabled,
summary = summary,
isError = value.toString() != valueState,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Done
),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
onValueChanged = {
if (it.length <= 1 || it.first() in decimalSeparators) valueState = it
else it.toDoubleOrNull()?.let { double ->
if (it.length <= 1 || it.first() in decimalSeparators) {
valueState = it
onValueChanged(double)
} else {
it.toDoubleOrNull()?.let { double ->
valueState = it
onValueChanged(double)
}
}
},
onFocusChanged = {},
modifier = modifier
modifier = modifier,
)
}
@ -192,6 +199,7 @@ fun EditTextPreference(
keyboardActions: KeyboardActions,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
maxSize: Int = 0, // max_size - 1 (in bytes)
onFocusChanged: (FocusState) -> Unit = {},
trailingIcon: (@Composable () -> Unit)? = null,
@ -199,49 +207,60 @@ fun EditTextPreference(
) {
var isFocused by remember { mutableStateOf(false) }
TextField(
value = value,
singleLine = true,
modifier = modifier
.fillMaxWidth()
.onFocusEvent { isFocused = it.isFocused; onFocusChanged(it) },
enabled = enabled,
isError = isError,
onValueChange = {
if (maxSize > 0) {
if (it.toByteArray().size <= maxSize) {
Column(modifier = modifier.padding(vertical = 8.dp)) {
OutlinedTextField(
value = value,
singleLine = true,
modifier =
Modifier.fillMaxWidth().onFocusEvent {
isFocused = it.isFocused
onFocusChanged(it)
},
enabled = enabled,
isError = isError,
onValueChange = {
if (maxSize > 0) {
if (it.toByteArray().size <= maxSize) {
onValueChanged(it)
}
} else {
onValueChanged(it)
}
} else onValueChanged(it)
},
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
trailingIcon = {
if (trailingIcon != null) {
trailingIcon()
} else if (isError) {
Icon(
imageVector = Icons.TwoTone.Info,
contentDescription = stringResource(id = R.string.error),
tint = MaterialTheme.colorScheme.error
},
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
trailingIcon = {
if (trailingIcon != null) {
trailingIcon()
} else if (isError) {
Icon(
imageVector = Icons.TwoTone.Info,
contentDescription = stringResource(id = R.string.error),
tint = MaterialTheme.colorScheme.error,
)
}
},
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
if (maxSize > 0 && isFocused) {
Box(contentAlignment = Alignment.BottomEnd, modifier = Modifier.fillMaxWidth()) {
Text(
text = "${value.toByteArray().size}/$maxSize",
style = MaterialTheme.typography.bodySmall,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 8.dp, bottom = 4.dp),
)
}
},
)
if (maxSize > 0 && isFocused) {
Box(
contentAlignment = Alignment.BottomEnd,
modifier = modifier.fillMaxWidth()
) {
Text(
text = "${value.toByteArray().size}/$maxSize",
style = MaterialTheme.typography.bodySmall,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 8.dp, bottom = 4.dp)
)
}
}
}
@ -253,6 +272,7 @@ private fun EditTextPreferencePreview() {
EditTextPreference(
title = "String",
value = "Meshtastic",
summary = "This is a summary",
maxSize = 39,
enabled = true,
isError = false,
@ -265,7 +285,7 @@ private fun EditTextPreferencePreview() {
value = UInt.MAX_VALUE.toInt(),
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {}
onValueChanged = {},
)
}
}

View file

@ -80,8 +80,18 @@ fun RadioConfigItemList(
if (isManaged) {
ManagedMessage()
}
ConfigRoute.radioConfigRoutes.forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
}
}
ConfigRoute.filterExcludedFrom(state.metadata).forEach {
TitledCard(title = stringResource(R.string.device_configuration), modifier = Modifier.padding(top = 16.dp)) {
if (isManaged) {
ManagedMessage()
}
ConfigRoute.deviceConfigRoutes(state.metadata).forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}

View file

@ -60,33 +60,33 @@ import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
private val DeviceConfig.Role.stringRes: Int
private val DeviceConfig.Role.description: Int
get() =
when (this) {
DeviceConfig.Role.CLIENT -> R.string.role_client
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute
DeviceConfig.Role.ROUTER -> R.string.role_router
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client
DeviceConfig.Role.REPEATER -> R.string.role_repeater
DeviceConfig.Role.TRACKER -> R.string.role_tracker
DeviceConfig.Role.SENSOR -> R.string.role_sensor
DeviceConfig.Role.TAK -> R.string.role_tak
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late
DeviceConfig.Role.CLIENT -> R.string.role_client_desc
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute_desc
DeviceConfig.Role.ROUTER -> R.string.role_router_desc
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client_desc
DeviceConfig.Role.REPEATER -> R.string.role_repeater_desc
DeviceConfig.Role.TRACKER -> R.string.role_tracker_desc
DeviceConfig.Role.SENSOR -> R.string.role_sensor_desc
DeviceConfig.Role.TAK -> R.string.role_tak_desc
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden_desc
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found_desc
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker_desc
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late_desc
else -> R.string.unrecognized
}
private val DeviceConfig.RebroadcastMode.stringRes: Int
private val DeviceConfig.RebroadcastMode.description: Int
get() =
when (this) {
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all_desc
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding_desc
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only_desc
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only_desc
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none_desc
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only_desc
else -> R.string.unrecognized
}
@ -160,60 +160,37 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.device_config)) }
item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
DropDownPreference(
title = stringResource(R.string.role),
enabled = enabled,
selectedItem = deviceInput.role,
onItemSelected = { selectedRole = it },
summary = stringResource(id = deviceInput.role.stringRes),
)
HorizontalDivider()
}
item {
EditTextPreference(
title = stringResource(R.string.redefine_pin_button),
value = deviceInput.buttonGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } },
summary = stringResource(id = deviceInput.role.description),
)
}
item {
EditTextPreference(
title = stringResource(R.string.redefine_pin_buzzer),
value = deviceInput.buzzerGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.rebroadcast_mode),
enabled = enabled,
selectedItem = deviceInput.rebroadcastMode,
onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } },
summary = stringResource(id = deviceInput.rebroadcastMode.stringRes),
summary = stringResource(id = deviceInput.rebroadcastMode.description),
)
HorizontalDivider()
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.nodeinfo_broadcast_interval_seconds),
title = stringResource(R.string.nodeinfo_broadcast_interval),
value = deviceInput.nodeInfoBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it } },
)
}
item { PreferenceCategory(text = stringResource(R.string.hardware)) }
item {
SwitchPreference(
title = stringResource(R.string.double_tap_as_button_press),
@ -222,24 +199,35 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } },
)
HorizontalDivider()
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.disable_triple_click),
summary = stringResource(id = R.string.config_device_disableTripleClick_summary),
checked = deviceInput.disableTripleClick,
title = stringResource(R.string.triple_click_adhoc_ping),
summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary),
checked = !deviceInput.disableTripleClick,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } },
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = !it } },
)
HorizontalDivider()
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.led_heartbeat),
summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary),
checked = !deviceInput.ledHeartbeatDisabled,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = !it } },
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.debug)) }
item {
EditTextPreference(
title = stringResource(R.string.posix_timezone),
title = stringResource(R.string.time_zone),
value = deviceInput.tzdef,
summary = stringResource(id = R.string.config_device_tzdef_summary),
maxSize = 64, // tzdef max_size:65
enabled = enabled,
isError = false,
@ -250,17 +238,25 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli
)
}
item { PreferenceCategory(text = stringResource(R.string.gpio)) }
item {
SwitchPreference(
title = stringResource(R.string.disable_led_heartbeat),
summary = stringResource(id = R.string.config_device_ledHeartbeatDisabled_summary),
checked = deviceInput.ledHeartbeatDisabled,
EditTextPreference(
title = stringResource(R.string.button_gpio),
value = deviceInput.buttonGpio,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.buzzer_gpio),
value = deviceInput.buzzerGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } },
)
HorizontalDivider()
}
item {
PreferenceFooter(
enabled = enabled && deviceInput != deviceConfig,

View file

@ -69,52 +69,40 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.display_config)) }
item {
EditTextPreference(
title = stringResource(R.string.screen_timeout_seconds),
value = displayInput.screenOnSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.auto_screen_carousel_seconds),
value = displayInput.autoScreenCarouselSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.compass_north_top),
title = stringResource(R.string.always_point_north),
summary = stringResource(id = R.string.config_display_compass_north_top_summary),
checked = displayInput.compassNorthTop,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.flip_screen),
checked = displayInput.flipScreen,
title = stringResource(R.string.use_12h_format),
summary = stringResource(R.string.display_time_in_12h_format),
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } },
checked = displayInput.use12HClock,
onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.bold_heading),
summary = stringResource(id = R.string.config_display_heading_bold_summary),
checked = displayInput.headingBold,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { headingBold = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.display_units),
summary = stringResource(id = R.string.config_display_units_summary),
enabled = enabled,
items =
DisplayConfig.DisplayUnits.entries
@ -126,23 +114,54 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
DropDownPreference(
title = stringResource(R.string.override_oled_auto_detect),
EditTextPreference(
title = stringResource(R.string.screen_on_for),
summary = stringResource(id = R.string.config_display_screen_on_secs_summary),
value = displayInput.screenOnSecs,
enabled = enabled,
items =
DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.oled,
onItemSelected = { displayInput = displayInput.copy { oled = it } },
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.carousel_interval),
summary = stringResource(id = R.string.config_display_auto_screen_carousel_secs_summary),
value = displayInput.autoScreenCarouselSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.wake_on_tap_or_motion),
summary = stringResource(id = R.string.config_display_wake_on_tap_or_motion_summary),
checked = displayInput.wakeOnTapOrMotion,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.flip_screen),
summary = stringResource(id = R.string.config_display_flip_screen_summary),
checked = displayInput.flipScreen,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.display_mode),
summary = stringResource(id = R.string.config_display_displaymode_summary),
enabled = enabled,
items =
DisplayConfig.DisplayMode.entries
@ -153,27 +172,20 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.heading_bold),
checked = displayInput.headingBold,
DropDownPreference(
title = stringResource(R.string.oled_type),
summary = stringResource(id = R.string.config_display_oled_summary),
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { headingBold = it } },
items =
DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.oled,
onItemSelected = { displayInput = displayInput.copy { oled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.wake_screen_on_tap_or_motion),
checked = displayInput.wakeOnTapOrMotion,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.compass_orientation),
@ -188,17 +200,6 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_12h_format),
summary = stringResource(R.string.display_time_in_12h_format),
enabled = enabled,
checked = displayInput.use12HClock,
onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && displayInput != displayConfig,

View file

@ -83,8 +83,18 @@ fun LoRaConfigItemList(
val primaryChannel by remember(loraInput) { mutableStateOf(Channel(primarySettings, loraInput)) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.lora_config)) }
item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
DropDownPreference(
title = stringResource(R.string.region_frequency_plan),
summary = stringResource(id = R.string.config_lora_region_summary),
enabled = enabled,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_modem_preset),
@ -99,6 +109,7 @@ fun LoRaConfigItemList(
item {
DropDownPreference(
title = stringResource(R.string.modem_preset),
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
enabled = enabled && loraInput.usePreset,
items =
LoRaConfig.ModemPreset.entries
@ -140,37 +151,26 @@ fun LoRaConfigItemList(
)
}
}
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
EditTextPreference(
title = stringResource(R.string.frequency_offset_mhz),
value = loraInput.frequencyOffset,
SwitchPreference(
title = stringResource(R.string.ignore_mqtt),
checked = loraInput.ignoreMqtt,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.region_frequency_plan),
enabled = enabled,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } },
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.hop_limit),
value = loraInput.hopLimit,
SwitchPreference(
title = stringResource(R.string.ok_to_mqtt),
checked = loraInput.configOkToMqtt,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } },
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
@ -181,21 +181,23 @@ fun LoRaConfigItemList(
)
}
item { HorizontalDivider() }
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = loraInput.txPower,
EditTextPreference(
title = stringResource(R.string.hop_limit),
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
value = loraInput.hopLimit,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { txPower = it } },
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } },
)
}
item { HorizontalDivider() }
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.frequency_slot),
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -207,17 +209,7 @@ fun LoRaConfigItemList(
},
)
}
item {
SwitchPreference(
title = stringResource(R.string.override_duty_cycle),
checked = loraInput.overrideDutyCycle,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.sx126x_rx_boosted_gain),
@ -227,7 +219,6 @@ fun LoRaConfigItemList(
)
}
item { HorizontalDivider() }
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(
@ -244,6 +235,16 @@ fun LoRaConfigItemList(
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } },
)
}
item { HorizontalDivider() }
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = loraInput.txPower,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { txPower = it } },
)
}
if (hasPaFan) {
item {
@ -257,26 +258,6 @@ fun LoRaConfigItemList(
item { HorizontalDivider() }
}
item {
SwitchPreference(
title = stringResource(R.string.ignore_mqtt),
checked = loraInput.ignoreMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.ok_to_mqtt),
checked = loraInput.configOkToMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && loraInput != loraConfig,

View file

@ -131,63 +131,87 @@ fun NetworkConfigItemList(
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.network_config)) }
if (hasWifi) {
item { PreferenceCategory(text = stringResource(R.string.wifi_config)) }
item {
SwitchPreference(
title = stringResource(R.string.wifi_enabled),
summary = stringResource(id = R.string.config_network_wifi_enabled_summary),
checked = networkInput.wifiEnabled,
enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } },
)
HorizontalDivider()
}
item {
SwitchPreference(
title = stringResource(R.string.wifi_enabled),
checked = networkInput.wifiEnabled,
enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } },
)
HorizontalDivider()
item {
EditTextPreference(
title = stringResource(R.string.ssid),
value = networkInput.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = enabled && hasWifi,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } },
)
}
item {
EditPasswordPreference(
title = stringResource(R.string.password),
value = networkInput.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = enabled && hasWifi,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } },
)
}
item {
Button(
onClick = { zxingScan() },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = enabled && hasWifi,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
}
}
}
item {
EditTextPreference(
title = stringResource(R.string.ssid),
value = networkInput.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = enabled && hasWifi,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } },
)
}
item {
EditPasswordPreference(
title = stringResource(R.string.psk),
value = networkInput.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = enabled && hasWifi,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } },
)
}
item {
Button(
onClick = { zxingScan() },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = enabled && hasWifi,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
if (hasEthernet) {
item { PreferenceCategory(text = stringResource(R.string.ethernet_config)) }
item {
SwitchPreference(
title = stringResource(R.string.ethernet_enabled),
summary = stringResource(id = R.string.config_network_eth_enabled_summary),
checked = networkInput.ethEnabled,
enabled = enabled && hasEthernet,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } },
)
HorizontalDivider()
}
}
item {
SwitchPreference(
title = stringResource(R.string.ethernet_enabled),
checked = networkInput.ethEnabled,
enabled = enabled && hasEthernet,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } },
)
HorizontalDivider()
if (hasEthernet || hasWifi) {
item { PreferenceCategory(text = stringResource(R.string.udp_config)) }
item {
SwitchPreference(
title = stringResource(R.string.udp_enabled),
summary = stringResource(id = R.string.config_network_udp_enabled_summary),
checked = networkInput.enabledProtocols == 1,
enabled = enabled,
onCheckedChange = {
networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
},
)
}
item { HorizontalDivider() }
}
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
EditTextPreference(
title = stringResource(R.string.ntp_server),
@ -282,22 +306,7 @@ fun NetworkConfigItemList(
)
}
item { HorizontalDivider() }
if (hasEthernet || hasWifi) {
item { PreferenceCategory(text = stringResource(R.string.udp_config)) }
item {
SwitchPreference(
title = stringResource(R.string.mesh_via_udp_enabled),
checked = networkInput.enabledProtocols == 1,
enabled = enabled,
onCheckedChange = {
networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
},
)
}
item { HorizontalDivider() }
}
item {
PreferenceFooter(
enabled = enabled && networkInput != networkConfig,

View file

@ -144,11 +144,12 @@ fun PositionConfigItemList(
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.position_config)) }
item { PreferenceCategory(text = stringResource(R.string.position_packet)) }
item {
EditTextPreference(
title = stringResource(R.string.position_broadcast_interval_seconds),
title = stringResource(R.string.broadcast_interval),
summary = stringResource(id = R.string.config_position_broadcast_secs_summary),
value = positionInput.positionBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -158,7 +159,7 @@ fun PositionConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.smart_position_enabled),
title = stringResource(R.string.smart_position),
checked = positionInput.positionBroadcastSmartEnabled,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { positionBroadcastSmartEnabled = it } },
@ -169,28 +170,30 @@ fun PositionConfigItemList(
if (positionInput.positionBroadcastSmartEnabled) {
item {
EditTextPreference(
title = stringResource(R.string.smart_broadcast_minimum_distance_meters),
value = positionInput.broadcastSmartMinimumDistance,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.smart_broadcast_minimum_interval_seconds),
title = stringResource(R.string.minimum_interval),
summary =
stringResource(id = R.string.config_position_broadcast_smart_minimum_interval_secs_summary),
value = positionInput.broadcastSmartMinimumIntervalSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.minimum_distance),
summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary),
value = positionInput.broadcastSmartMinimumDistance,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } },
)
}
}
item { PreferenceCategory(text = stringResource(R.string.device_gps)) }
item {
SwitchPreference(
title = stringResource(R.string.use_fixed_position),
title = stringResource(R.string.fixed_position),
checked = positionInput.fixedPosition,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } },
@ -227,7 +230,7 @@ fun PositionConfigItemList(
}
item {
EditTextPreference(
title = stringResource(R.string.altitude_meters),
title = stringResource(R.string.altitude),
value = locationInput.altitude,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -260,17 +263,19 @@ fun PositionConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.gps_update_interval_seconds),
title = stringResource(R.string.update_interval),
summary = stringResource(id = R.string.config_position_gps_update_interval_summary),
value = positionInput.gpsUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } },
)
}
item { PreferenceCategory(text = stringResource(R.string.position_flags)) }
item {
BitwisePreference(
title = stringResource(R.string.position_flags),
summary = stringResource(id = R.string.config_position_flags_summary),
value = positionInput.positionFlags,
enabled = enabled,
items =
@ -283,10 +288,11 @@ fun PositionConfigItemList(
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.advanced_device_gps)) }
item {
EditTextPreference(
title = stringResource(R.string.redefine_gps_rx_pin),
title = stringResource(R.string.gps_receive_gpio),
value = positionInput.rxGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -296,7 +302,7 @@ fun PositionConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.redefine_gps_tx_pin),
title = stringResource(R.string.gps_transmit_gpio),
value = positionInput.txGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -306,7 +312,7 @@ fun PositionConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.redefine_pin_gps_en),
title = stringResource(R.string.gps_en_gpio),
value = positionInput.gpsEnGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),

View file

@ -65,6 +65,8 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var powerInput by rememberSaveable { mutableStateOf(powerConfig) }
var shutdownOnPowerLoss by rememberSaveable { mutableStateOf(powerConfig.onBatteryShutdownAfterSecs > 0) }
var adcOverride by rememberSaveable { mutableStateOf(powerConfig.adcMultiplierOverride > 0f) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.power_config)) }
@ -72,6 +74,7 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke
item {
SwitchPreference(
title = stringResource(R.string.enable_power_saving_mode),
summary = stringResource(id = R.string.config_power_is_power_saving_summary),
checked = powerInput.isPowerSaving,
enabled = enabled,
onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } },
@ -80,25 +83,57 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.shutdown_on_battery_delay_seconds),
value = powerInput.onBatteryShutdownAfterSecs,
SwitchPreference(
title = stringResource(R.string.shutdown_on_power_loss),
checked = shutdownOnPowerLoss,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } },
onCheckedChange = {
shutdownOnPowerLoss = it
if (!it) powerInput = powerInput.copy { onBatteryShutdownAfterSecs = 0 }
},
)
}
if (shutdownOnPowerLoss) {
item {
EditTextPreference(
title = stringResource(R.string.shutdown_on_battery_delay_seconds),
value = powerInput.onBatteryShutdownAfterSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } },
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.adc_multiplier_override_ratio),
value = powerInput.adcMultiplierOverride,
SwitchPreference(
title = stringResource(R.string.adc_multiplier_override),
checked = adcOverride,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } },
onCheckedChange = {
adcOverride = it
if (!it) powerInput = powerInput.copy { adcMultiplierOverride = 0f }
},
)
}
if (adcOverride) {
item {
EditTextPreference(
title = stringResource(R.string.adc_multiplier_override_ratio),
value = powerInput.adcMultiplierOverride,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } },
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.wait_for_bluetooth_duration_seconds),

View file

@ -154,18 +154,19 @@ fun SecurityConfigItemList(
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.security_config)) }
item { PreferenceCategory(text = stringResource(R.string.direct_message_key)) }
item {
EditBase64Preference(
title = stringResource(R.string.public_key),
summary = stringResource(id = R.string.config_security_public_key),
value = publicKey,
enabled = enabled,
readOnly = true,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
securityInput = securityInput.copy { publicKey = it }
securityInput = securityInput.copy { this.publicKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = securityInput.publicKey.encodeToString()) },
@ -175,6 +176,7 @@ fun SecurityConfigItemList(
item {
EditBase64Preference(
title = stringResource(R.string.private_key),
summary = stringResource(id = R.string.config_security_private_key),
value = securityInput.privateKey,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
@ -206,10 +208,11 @@ fun SecurityConfigItemList(
onClick = { showEditSecurityConfigDialog = true },
)
}
item { PreferenceCategory(text = stringResource(R.string.admin_keys)) }
item {
EditListPreference(
title = stringResource(R.string.admin_key),
summary = stringResource(id = R.string.config_security_admin_key),
list = securityInput.adminKeyList,
maxCount = 3,
enabled = enabled,
@ -223,20 +226,11 @@ fun SecurityConfigItemList(
},
)
}
item {
SwitchPreference(
title = stringResource(R.string.managed_mode),
checked = securityInput.isManaged,
enabled = enabled && securityInput.adminKeyCount > 0,
onCheckedChange = { securityInput = securityInput.copy { isManaged = it } },
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.logs)) }
item {
SwitchPreference(
title = stringResource(R.string.serial_console),
summary = stringResource(id = R.string.config_security_serial_enabled),
checked = securityInput.serialEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } },
@ -247,12 +241,24 @@ fun SecurityConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.debug_log_api_enabled),
summary = stringResource(id = R.string.config_security_debug_log_api_enabled),
checked = securityInput.debugLogApiEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { debugLogApiEnabled = it } },
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.administration)) }
item {
SwitchPreference(
title = stringResource(R.string.managed_mode),
summary = stringResource(id = R.string.config_security_is_managed),
checked = securityInput.isManaged,
enabled = enabled && securityInput.adminKeyCount > 0,
onCheckedChange = { securityInput = securityInput.copy { isManaged = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(