feat(settings): replace interval inputs with dropdowns (#3352)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-10-09 14:03:06 -05:00 committed by GitHub
parent e5a28d6942
commit c6be5be72f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2343 additions and 2028 deletions

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
@ -32,7 +33,6 @@ 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.protobuf.ProtocolMessageEnum
@ -84,49 +84,45 @@ fun <T> DropDownPreference(
emptyList()
}
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = "",
onValueChange = {},
prefix = { Text(title) },
suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors =
ExposedDropdownMenuDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
enabled = enabled,
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
onItemSelected(selectionOption.first)
expanded = false
},
)
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
) {
OutlinedTextField(
label = { Text(text = title) },
modifier =
Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
onValueChange = {},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
enabled = enabled,
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
onItemSelected(selectionOption.first)
expanded = false
},
)
}
}
}
}
}

View file

@ -26,10 +26,8 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -40,12 +38,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@ -211,15 +207,11 @@ fun EditTextPreference(
) {
var isFocused by remember { mutableStateOf(false) }
Column(modifier = modifier) {
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().onFocusEvent { onFocusChanged(it) },
value = value,
singleLine = true,
modifier =
Modifier.fillMaxWidth().onFocusEvent {
isFocused = it.isFocused
onFocusChanged(it)
},
enabled = enabled,
isError = isError,
onValueChange = {
@ -231,7 +223,7 @@ fun EditTextPreference(
onValueChanged(it)
}
},
prefix = { Text(title) },
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
@ -249,14 +241,6 @@ fun EditTextPreference(
} else {
null
},
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
)
if (summary != null) {
Text(

View file

@ -0,0 +1,112 @@
/*
* 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 org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ListItem
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
import kotlin.math.roundToInt
@Composable
fun <T> SliderPreference(
title: String,
enabled: Boolean,
items: List<Pair<T, String>>,
selectedValue: T,
onValueChange: (T) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
if (items.isEmpty()) return
val selectedIndex = items.indexOfFirst { it.first == selectedValue }.toFloat()
val valueRange = 0f..(items.size - 1).toFloat()
val steps = (items.size - 2).coerceAtLeast(0)
ListItem(
modifier = modifier,
headlineContent = {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End,
text = items.firstOrNull { it.first == selectedValue }?.second ?: items.first().second,
)
},
overlineContent = { Text(text = title) },
supportingContent = {
Column {
summary?.let { Text(text = it, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) }
Slider(
value = selectedIndex.coerceIn(valueRange),
onValueChange = {
val index = it.roundToInt()
if (index in items.indices) {
onValueChange(items[index].first)
}
},
valueRange = valueRange,
steps = steps,
enabled = enabled,
)
}
},
)
}
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferencePreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
title = "Slider",
summary = "Select a value",
enabled = true,
items = items,
selectedValue = 3L,
onValueChange = {},
)
}
}
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferenceDisabledPreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
title = "Slider",
summary = "Select a value",
enabled = false,
items = items,
selectedValue = 3L,
onValueChange = {},
)
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -28,8 +29,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -53,56 +54,46 @@ fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioCo
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.ambient_lighting_config)) }
item {
SwitchPreference(
title = stringResource(R.string.led_state),
checked = formState.value.ledState,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ledState = it } },
)
}
item { HorizontalDivider() }
TitledCard(title = stringResource(R.string.ambient_lighting_config)) {
SwitchPreference(
title = stringResource(R.string.led_state),
checked = formState.value.ledState,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ledState = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.current),
value = formState.value.current,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { current = it } },
)
EditTextPreference(
title = stringResource(R.string.red),
value = formState.value.red,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { red = it } },
)
EditTextPreference(
title = stringResource(R.string.green),
value = formState.value.green,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { green = it } },
)
item {
EditTextPreference(
title = stringResource(R.string.current),
value = formState.value.current,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { current = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.red),
value = formState.value.red,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { red = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.green),
value = formState.value.green,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { green = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.blue),
value = formState.value.blue,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { blue = it } },
)
EditTextPreference(
title = stringResource(R.string.blue),
value = formState.value.blue,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { blue = it } },
)
}
}
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -29,8 +30,8 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.AudioConfig
import org.meshtastic.proto.copy
@ -55,80 +56,63 @@ fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewMo
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.audio_config)) }
item {
SwitchPreference(
title = stringResource(R.string.codec_2_enabled),
checked = formState.value.codec2Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { codec2Enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.ptt_pin),
value = formState.value.pttPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { pttPin = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.codec2_sample_rate),
enabled = state.connected,
items =
AudioConfig.Audio_Baud.entries
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.bitrate,
onItemSelected = { formState.value = formState.value.copy { bitrate = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.i2s_word_select),
value = formState.value.i2SWs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SWs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_data_in),
value = formState.value.i2SSd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SSd = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_data_out),
value = formState.value.i2SDin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SDin = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_clock),
value = formState.value.i2SSck,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SSck = it } },
)
TitledCard(title = stringResource(R.string.audio_config)) {
SwitchPreference(
title = stringResource(R.string.codec_2_enabled),
checked = formState.value.codec2Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { codec2Enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.ptt_pin),
value = formState.value.pttPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { pttPin = it } },
)
DropDownPreference(
title = stringResource(R.string.codec2_sample_rate),
enabled = state.connected,
items =
AudioConfig.Audio_Baud.entries
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.bitrate,
onItemSelected = { formState.value = formState.value.copy { bitrate = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.i2s_word_select),
value = formState.value.i2SWs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SWs = it } },
)
EditTextPreference(
title = stringResource(R.string.i2s_data_in),
value = formState.value.i2SSd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SSd = it } },
)
EditTextPreference(
title = stringResource(R.string.i2s_data_out),
value = formState.value.i2SDin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SDin = it } },
)
EditTextPreference(
title = stringResource(R.string.i2s_clock),
value = formState.value.i2SSck,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { i2SSck = it } },
)
}
}
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -29,8 +30,8 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ConfigProtos.Config.BluetoothConfig
import org.meshtastic.proto.config
@ -55,44 +56,39 @@ fun BluetoothConfigScreen(navController: NavController, viewModel: RadioConfigVi
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.bluetooth_config)) }
item {
SwitchPreference(
title = stringResource(R.string.bluetooth_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.pairing_mode),
enabled = state.connected,
items =
BluetoothConfig.PairingMode.entries
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.fixed_pin),
value = formState.value.fixedPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it.toString().length == 6) { // ensure 6 digits
formState.value = formState.value.copy { fixedPin = it }
}
},
)
TitledCard(title = stringResource(R.string.bluetooth_config)) {
SwitchPreference(
title = stringResource(R.string.bluetooth_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.pairing_mode),
enabled = state.connected,
items =
BluetoothConfig.PairingMode.entries
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.fixed_pin),
value = formState.value.fixedPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it.toString().length == 6) { // ensure 6 digits
formState.value = formState.value.copy { fixedPin = it }
}
},
)
}
}
}
}

View file

@ -19,6 +19,7 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -35,8 +36,8 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import org.meshtastic.proto.copy
@ -68,146 +69,117 @@ fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConf
}
},
) {
item { PreferenceCategory(text = stringResource(R.string.canned_message_config)) }
item {
SwitchPreference(
title = stringResource(R.string.canned_message_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.rotary_encoder_1_enabled),
checked = formState.value.rotary1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { rotary1Enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_a_port),
value = formState.value.inputbrokerPinA,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinA = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_b_port),
value = formState.value.inputbrokerPinB,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinB = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_press_port),
value = formState.value.inputbrokerPinPress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinPress = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_press),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventPress,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventPress = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_cw),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventCw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCw = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_ccw),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventCcw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCcw = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.up_down_select_input_enabled),
checked = formState.value.updown1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { updown1Enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.allow_input_source),
value = formState.value.allowInputSource,
maxSize = 63, // allow_input_source max_size:16
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { allowInputSource = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.send_bell),
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.messages),
value = messagesInput,
maxSize = 200, // messages max_size:201
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { messagesInput = it },
)
TitledCard(title = stringResource(R.string.canned_message_config)) {
SwitchPreference(
title = stringResource(R.string.canned_message_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.rotary_encoder_1_enabled),
checked = formState.value.rotary1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { rotary1Enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_a_port),
value = formState.value.inputbrokerPinA,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinA = it } },
)
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_b_port),
value = formState.value.inputbrokerPinB,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinB = it } },
)
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_press_port),
value = formState.value.inputbrokerPinPress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinPress = it } },
)
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_press),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventPress,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventPress = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_cw),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventCw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCw = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_ccw),
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.inputbrokerEventCcw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCcw = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.up_down_select_input_enabled),
checked = formState.value.updown1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { updown1Enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.allow_input_source),
value = formState.value.allowInputSource,
maxSize = 63, // allow_input_source max_size:16
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { allowInputSource = it } },
)
SwitchPreference(
title = stringResource(R.string.send_bell),
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.messages),
value = messagesInput,
maxSize = 200, // messages max_size:201
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { messagesInput = it },
)
}
}
}
}

View file

@ -19,9 +19,11 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -32,9 +34,12 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.gpioPins
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -58,94 +63,85 @@ fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioCo
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.detection_sensor_config)) }
item {
SwitchPreference(
title = stringResource(R.string.detection_sensor_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
TitledCard(title = stringResource(R.string.detection_sensor_config)) {
SwitchPreference(
title = stringResource(R.string.detection_sensor_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val minimumBroadcastIntervals = remember {
IntervalConfiguration.DETECTION_SENSOR_MINIMUM.allowedIntervals
}
DropDownPreference(
title = stringResource(R.string.minimum_broadcast_seconds),
selectedItem = formState.value.minimumBroadcastSecs.toLong(),
enabled = state.connected,
items = minimumBroadcastIntervals.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { minimumBroadcastSecs = it.toInt() } },
)
item {
EditTextPreference(
title = stringResource(R.string.minimum_broadcast_seconds),
value = formState.value.minimumBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { minimumBroadcastSecs = it } },
)
val stateBroadcastIntervals = remember { IntervalConfiguration.DETECTION_SENSOR_STATE.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.state_broadcast_seconds),
selectedItem = formState.value.stateBroadcastSecs.toLong(),
enabled = state.connected,
items = stateBroadcastIntervals.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { stateBroadcastSecs = it.toInt() } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.send_bell_with_alert_message),
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.friendly_name),
value = formState.value.name,
maxSize = 19, // name max_size:20
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { name = it } },
)
HorizontalDivider()
val pins = remember { gpioPins }
DropDownPreference(
title = stringResource(R.string.gpio_pin_to_monitor),
items = pins,
selectedItem = formState.value.monitorPin,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { monitorPin = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.detection_trigger_type),
enabled = state.connected,
items =
ModuleConfig.DetectionSensorConfig.TriggerType.entries
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.detectionTriggerType,
onItemSelected = { formState.value = formState.value.copy { detectionTriggerType = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.use_input_pullup_mode),
checked = formState.value.usePullup,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePullup = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
}
}
item {
EditTextPreference(
title = stringResource(R.string.state_broadcast_seconds),
value = formState.value.stateBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { stateBroadcastSecs = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.send_bell_with_alert_message),
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.friendly_name),
value = formState.value.name,
maxSize = 19, // name max_size:20
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { name = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_to_monitor),
value = formState.value.monitorPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { monitorPin = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.detection_trigger_type),
enabled = state.connected,
items =
ModuleConfig.DetectionSensorConfig.TriggerType.entries
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.detectionTriggerType,
onItemSelected = { formState.value = formState.value.copy { detectionTriggerType = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_input_pullup_mode),
checked = formState.value.usePullup,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePullup = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
@ -31,6 +32,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -50,9 +52,11 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ConfigProtos.Config.DeviceConfig
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
@ -117,102 +121,105 @@ fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewM
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
DropDownPreference(
title = stringResource(R.string.role),
enabled = state.connected,
selectedItem = formState.value.role,
onItemSelected = { selectedRole = it },
summary = stringResource(id = formState.value.role.description),
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.rebroadcast_mode),
enabled = state.connected,
selectedItem = formState.value.rebroadcastMode,
onItemSelected = { formState.value = formState.value.copy { rebroadcastMode = it } },
summary = stringResource(id = formState.value.rebroadcastMode.description),
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.nodeinfo_broadcast_interval),
value = formState.value.nodeInfoBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { nodeInfoBroadcastSecs = it } },
)
}
item { PreferenceCategory(text = stringResource(R.string.hardware)) }
item {
SwitchPreference(
title = stringResource(R.string.double_tap_as_button_press),
summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary),
checked = formState.value.doubleTapAsButtonPress,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { doubleTapAsButtonPress = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.triple_click_adhoc_ping),
summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary),
checked = !formState.value.disableTripleClick,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { disableTripleClick = !it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.led_heartbeat),
summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary),
checked = !formState.value.ledHeartbeatDisabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ledHeartbeatDisabled = !it } },
)
}
item { HorizontalDivider() }
TitledCard(title = stringResource(R.string.options)) {
DropDownPreference(
title = stringResource(R.string.role),
enabled = state.connected,
selectedItem = formState.value.role,
onItemSelected = { selectedRole = it },
summary = stringResource(id = formState.value.role.description),
)
HorizontalDivider()
item { PreferenceCategory(text = stringResource(R.string.debug)) }
DropDownPreference(
title = stringResource(R.string.rebroadcast_mode),
enabled = state.connected,
selectedItem = formState.value.rebroadcastMode,
onItemSelected = { formState.value = formState.value.copy { rebroadcastMode = it } },
summary = stringResource(id = formState.value.rebroadcastMode.description),
)
HorizontalDivider()
val nodeInfoBroadcastIntervals = remember { IntervalConfiguration.NODE_INFO_BROADCAST.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.nodeinfo_broadcast_interval),
selectedItem = formState.value.nodeInfoBroadcastSecs.toLong(),
enabled = state.connected,
items = nodeInfoBroadcastIntervals.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { nodeInfoBroadcastSecs = it.toInt() } },
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.time_zone),
value = formState.value.tzdef,
summary = stringResource(id = R.string.config_device_tzdef_summary),
maxSize = 64, // tzdef max_size:65
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { tzdef = it } },
)
TitledCard(title = stringResource(R.string.hardware)) {
SwitchPreference(
title = stringResource(R.string.double_tap_as_button_press),
summary = stringResource(id = R.string.config_device_doubleTapAsButtonPress_summary),
checked = formState.value.doubleTapAsButtonPress,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { doubleTapAsButtonPress = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.triple_click_adhoc_ping),
summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary),
checked = !formState.value.disableTripleClick,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { disableTripleClick = !it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.led_heartbeat),
summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary),
checked = !formState.value.ledHeartbeatDisabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ledHeartbeatDisabled = !it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
}
}
item {
TitledCard(title = stringResource(R.string.debug)) {
EditTextPreference(
title = stringResource(R.string.time_zone),
value = formState.value.tzdef,
summary = stringResource(id = R.string.config_device_tzdef_summary),
maxSize = 64, // tzdef max_size:65
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { tzdef = it } },
)
}
}
item { PreferenceCategory(text = stringResource(R.string.gpio)) }
item {
EditTextPreference(
title = stringResource(R.string.button_gpio),
value = formState.value.buttonGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { buttonGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.buzzer_gpio),
value = formState.value.buzzerGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { buzzerGpio = it } },
)
TitledCard(title = stringResource(R.string.gpio)) {
EditTextPreference(
title = stringResource(R.string.button_gpio),
value = formState.value.buttonGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { buttonGpio = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.buzzer_gpio),
value = formState.value.buzzerGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { buzzerGpio = it } },
)
}
}
}
}

View file

@ -17,21 +17,22 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
@ -41,7 +42,6 @@ fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigView
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val displayConfig = state.radioConfig.display
val formState = rememberConfigState(initialValue = displayConfig)
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.display),
@ -55,136 +55,129 @@ fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigView
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.display_config)) }
item {
SwitchPreference(
title = stringResource(R.string.always_point_north),
summary = stringResource(id = R.string.config_display_compass_north_top_summary),
checked = formState.value.compassNorthTop,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { compassNorthTop = it } },
)
TitledCard(title = stringResource(R.string.display_config)) {
SwitchPreference(
title = stringResource(R.string.always_point_north),
summary = stringResource(id = R.string.config_display_compass_north_top_summary),
checked = formState.value.compassNorthTop,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { compassNorthTop = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.use_12h_format),
summary = stringResource(R.string.display_time_in_12h_format),
enabled = state.connected,
checked = formState.value.use12HClock,
onCheckedChange = { formState.value = formState.value.copy { use12HClock = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.bold_heading),
summary = stringResource(id = R.string.config_display_heading_bold_summary),
checked = formState.value.headingBold,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { headingBold = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.display_units),
summary = stringResource(id = R.string.config_display_units_summary),
enabled = state.connected,
items =
DisplayConfig.DisplayUnits.entries
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.units,
onItemSelected = { formState.value = formState.value.copy { units = it } },
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_12h_format),
summary = stringResource(R.string.display_time_in_12h_format),
enabled = state.connected,
checked = formState.value.use12HClock,
onCheckedChange = { formState.value = formState.value.copy { use12HClock = it } },
)
TitledCard(title = stringResource(R.string.advanced)) {
val screenOnIntervals = remember { IntervalConfiguration.DISPLAY_SCREEN_ON.allowedIntervals }
val carouselIntervals = remember { IntervalConfiguration.DISPLAY_CAROUSEL.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.screen_on_for),
summary = stringResource(id = R.string.config_display_screen_on_secs_summary),
enabled = state.connected,
items = screenOnIntervals.map { it to it.toDisplayString() },
selectedItem =
screenOnIntervals.find { it.value == formState.value.screenOnSecs.toLong() }
?: screenOnIntervals.first(),
onItemSelected = { formState.value = formState.value.copy { screenOnSecs = it.value.toInt() } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.carousel_interval),
summary = stringResource(id = R.string.config_display_auto_screen_carousel_secs_summary),
enabled = state.connected,
items = carouselIntervals.map { it to it.toDisplayString() },
selectedItem =
carouselIntervals.find { it.value == formState.value.autoScreenCarouselSecs.toLong() }
?: carouselIntervals.first(),
onItemSelected = {
formState.value = formState.value.copy { autoScreenCarouselSecs = it.value.toInt() }
},
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.wake_on_tap_or_motion),
summary = stringResource(id = R.string.config_display_wake_on_tap_or_motion_summary),
checked = formState.value.wakeOnTapOrMotion,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wakeOnTapOrMotion = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.flip_screen),
summary = stringResource(id = R.string.config_display_flip_screen_summary),
checked = formState.value.flipScreen,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { flipScreen = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.display_mode),
summary = stringResource(id = R.string.config_display_displaymode_summary),
enabled = state.connected,
items =
DisplayConfig.DisplayMode.entries
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.displaymode,
onItemSelected = { formState.value = formState.value.copy { displaymode = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.oled_type),
summary = stringResource(id = R.string.config_display_oled_summary),
enabled = state.connected,
items =
DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.oled,
onItemSelected = { formState.value = formState.value.copy { oled = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.compass_orientation),
enabled = state.connected,
items =
DisplayConfig.CompassOrientation.entries
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.compassOrientation,
onItemSelected = { formState.value = formState.value.copy { compassOrientation = it } },
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.bold_heading),
summary = stringResource(id = R.string.config_display_heading_bold_summary),
checked = formState.value.headingBold,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { headingBold = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.display_units),
summary = stringResource(id = R.string.config_display_units_summary),
enabled = state.connected,
items =
DisplayConfig.DisplayUnits.entries
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.units,
onItemSelected = { formState.value = formState.value.copy { units = it } },
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
EditTextPreference(
title = stringResource(R.string.screen_on_for),
summary = stringResource(id = R.string.config_display_screen_on_secs_summary),
value = formState.value.screenOnSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.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 = formState.value.autoScreenCarouselSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.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 = formState.value.wakeOnTapOrMotion,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wakeOnTapOrMotion = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.flip_screen),
summary = stringResource(id = R.string.config_display_flip_screen_summary),
checked = formState.value.flipScreen,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { flipScreen = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.display_mode),
summary = stringResource(id = R.string.config_display_displaymode_summary),
enabled = state.connected,
items =
DisplayConfig.DisplayMode.entries
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.displaymode,
onItemSelected = { formState.value = formState.value.copy { displaymode = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.oled_type),
summary = stringResource(id = R.string.config_display_oled_summary),
enabled = state.connected,
items =
DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.oled,
onItemSelected = { formState.value = formState.value.copy { oled = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.compass_orientation),
enabled = state.connected,
items =
DisplayConfig.CompassOrientation.entries
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.compassOrientation,
onItemSelected = { formState.value = formState.value.copy { compassOrientation = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -19,10 +19,12 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalFocusManager
@ -33,11 +35,14 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TextDividerPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.gpioPins
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -67,183 +72,159 @@ fun ExternalNotificationConfigScreen(navController: NavController, viewModel: Ra
}
},
) {
item { PreferenceCategory(text = stringResource(R.string.external_notification_config)) }
item {
SwitchPreference(
title = stringResource(R.string.external_notification_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item {
TextDividerPreference(stringResource(R.string.notifications_on_message_receipt), enabled = state.connected)
}
item {
SwitchPreference(
title = stringResource(R.string.alert_message_led),
checked = formState.value.alertMessage,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessage = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.alert_message_buzzer),
checked = formState.value.alertMessageBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessageBuzzer = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.alert_message_vibra),
checked = formState.value.alertMessageVibra,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessageVibra = it } },
)
}
item {
TextDividerPreference(
stringResource(R.string.notifications_on_alert_bell_receipt),
enabled = state.connected,
)
}
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_led),
checked = formState.value.alertBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBell = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_buzzer),
checked = formState.value.alertBellBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellBuzzer = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_vibra),
checked = formState.value.alertBellVibra,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellVibra = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.output_led_gpio),
value = formState.value.output,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { output = it } },
)
}
if (formState.value.output != 0) {
item {
TitledCard(title = stringResource(R.string.external_notification_config)) {
SwitchPreference(
title = stringResource(R.string.output_led_active_high),
checked = formState.value.active,
title = stringResource(R.string.external_notification_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { active = it } },
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.output_buzzer_gpio),
value = formState.value.outputBuzzer,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { outputBuzzer = it } },
)
}
if (formState.value.outputBuzzer != 0) {
item {
TitledCard(title = stringResource(R.string.notifications_on_message_receipt)) {
SwitchPreference(
title = stringResource(R.string.use_pwm_buzzer),
checked = formState.value.usePwm,
title = stringResource(R.string.alert_message_led),
checked = formState.value.alertMessage,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePwm = it } },
onCheckedChange = { formState.value = formState.value.copy { alertMessage = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.alert_message_buzzer),
checked = formState.value.alertMessageBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessageBuzzer = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.alert_message_vibra),
checked = formState.value.alertMessageVibra,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessageVibra = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.output_vibra_gpio),
value = formState.value.outputVibra,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { outputVibra = it } },
)
TitledCard(title = stringResource(R.string.notifications_on_alert_bell_receipt)) {
SwitchPreference(
title = stringResource(R.string.alert_bell_led),
checked = formState.value.alertBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBell = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.alert_bell_buzzer),
checked = formState.value.alertBellBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellBuzzer = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.alert_bell_vibra),
checked = formState.value.alertBellVibra,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellVibra = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.output_duration_milliseconds),
value = formState.value.outputMs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { outputMs = it } },
)
TitledCard(title = stringResource(R.string.advanced)) {
val gpio = remember { gpioPins }
DropDownPreference(
title = stringResource(R.string.output_led_gpio),
items = gpio,
selectedItem = formState.value.output,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { output = it } },
)
if (formState.value.output != 0) {
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.output_led_active_high),
checked = formState.value.active,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { active = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.output_buzzer_gpio),
items = gpio,
selectedItem = formState.value.outputBuzzer,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { outputBuzzer = it } },
)
if (formState.value.outputBuzzer != 0) {
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.use_pwm_buzzer),
checked = formState.value.usePwm,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePwm = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.output_vibra_gpio),
items = gpio,
selectedItem = formState.value.outputVibra,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { outputVibra = it } },
)
HorizontalDivider()
val outputItems = remember { IntervalConfiguration.OUTPUT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.output_duration_milliseconds),
items = outputItems.map { it.value to it.toDisplayString() },
selectedItem = formState.value.outputMs,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { outputMs = it.toInt() } },
)
HorizontalDivider()
val nagItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.nag_timeout_seconds),
items = nagItems.map { it.value to it.toDisplayString() },
selectedItem = formState.value.nagTimeout,
enabled = state.connected,
onItemSelected = { formState.value = formState.value.copy { nagTimeout = it.toInt() } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.ringtone),
value = ringtoneInput,
maxSize = 230, // ringtone max_size:231
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ringtoneInput = it },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.use_i2s_as_buzzer),
checked = formState.value.useI2SAsBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { useI2SAsBuzzer = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.nag_timeout_seconds),
value = formState.value.nagTimeout,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { nagTimeout = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.ringtone),
value = ringtoneInput,
maxSize = 230, // ringtone max_size:231
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ringtoneInput = it },
)
}
item {
SwitchPreference(
title = stringResource(R.string.use_i2s_as_buzzer),
checked = formState.value.useI2SAsBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { useI2SAsBuzzer = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -17,19 +17,16 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.model.Channel
@ -39,11 +36,11 @@ import org.meshtastic.core.model.numChannels
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceDivider
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.hopLimits
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
@ -79,19 +76,15 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
selectedItem = formState.value.region,
onItemSelected = { formState.value = formState.value.copy { region = it } },
)
PreferenceDivider()
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.use_modem_preset),
checked = formState.value.usePreset,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
PreferenceDivider()
HorizontalDivider()
if (formState.value.usePreset) {
DropDownPreference(
title = stringResource(R.string.modem_preset),
@ -109,9 +102,7 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
)
PreferenceDivider()
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.spread_factor),
value = formState.value.spreadFactor,
@ -119,9 +110,7 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
)
PreferenceDivider()
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.coding_rate),
value = formState.value.codingRate,
@ -133,8 +122,6 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
}
}
item { Spacer(modifier = Modifier.height(16.dp)) }
item {
TitledCard(title = stringResource(R.string.advanced)) {
SwitchPreference(
@ -142,42 +129,35 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
checked = formState.value.ignoreMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
PreferenceDivider()
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.ok_to_mqtt),
checked = formState.value.configOkToMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
PreferenceDivider()
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.tx_enabled),
checked = formState.value.txEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
PreferenceDivider()
EditTextPreference(
HorizontalDivider()
val hopLimitItems = remember { hopLimits }
DropDownPreference(
title = stringResource(R.string.hop_limit),
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
value = formState.value.hopLimit,
items = hopLimitItems,
selectedItem = formState.value.hopLimit,
onItemSelected = { formState.value = formState.value.copy { hopLimit = it } },
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
)
PreferenceDivider()
HorizontalDivider()
var isFocusedSlot by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.frequency_slot),
@ -197,19 +177,15 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
}
},
)
PreferenceDivider()
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.sx126x_rx_boosted_gain),
checked = formState.value.sx126XRxBoostedGain,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
PreferenceDivider()
HorizontalDivider()
var isFocusedOverride by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.override_frequency_mhz),
@ -224,9 +200,7 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
onFocusChanged = { isFocusedOverride = it.isFocused },
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
)
PreferenceDivider()
HorizontalDivider()
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = formState.value.txPower,
@ -234,14 +208,14 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
)
if (viewModel.hasPaFan) {
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.pa_fan_disabled),
checked = formState.value.paFanDisabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
containerColor = Color.Transparent,
containerColor = CardDefaults.cardColors().containerColor,
)
}
}

View file

@ -21,6 +21,7 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -34,8 +35,8 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -77,141 +78,125 @@ fun MQTTConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.mqtt_config)) }
item {
SwitchPreference(
title = stringResource(R.string.mqtt_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.address),
value = formState.value.address,
maxSize = 63, // address max_size:64
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { address = it } },
)
TitledCard(title = stringResource(R.string.mqtt_config)) {
SwitchPreference(
title = stringResource(R.string.mqtt_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.address),
value = formState.value.address,
maxSize = 63, // address max_size:64
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { address = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.username),
value = formState.value.username,
maxSize = 63, // username max_size:64
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { username = it } },
)
HorizontalDivider()
EditPasswordPreference(
title = stringResource(R.string.password),
value = formState.value.password,
maxSize = 63, // password max_size:64
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { password = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.encryption_enabled),
checked = formState.value.encryptionEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { encryptionEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.json_output_enabled),
checked = formState.value.jsonEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { jsonEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val defaultAddress = stringResource(R.string.default_mqtt_address)
val isDefault = formState.value.address.isEmpty() || formState.value.address.contains(defaultAddress)
val enforceTls = isDefault && formState.value.proxyToClientEnabled
SwitchPreference(
title = stringResource(R.string.tls_enabled),
checked = formState.value.tlsEnabled || enforceTls,
enabled = state.connected && !enforceTls,
onCheckedChange = { formState.value = formState.value.copy { tlsEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.root_topic),
value = formState.value.root,
maxSize = 31, // root max_size:32
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { root = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.proxy_to_client_enabled),
checked = formState.value.proxyToClientEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { proxyToClientEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.username),
value = formState.value.username,
maxSize = 63, // username max_size:64
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { username = it } },
)
TitledCard(title = stringResource(R.string.map_reporting)) {
MapReportingPreference(
mapReportingEnabled = formState.value.mapReportingEnabled,
onMapReportingEnabledChanged = {
formState.value = formState.value.copy { mapReportingEnabled = it }
},
shouldReportLocation = formState.value.mapReportSettings.shouldReportLocation,
onShouldReportLocationChanged = {
viewModel.setShouldReportLocation(destNum, it)
val settings = formState.value.mapReportSettings.copy { this.shouldReportLocation = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
positionPrecision = formState.value.mapReportSettings.positionPrecision,
onPositionPrecisionChanged = {
val settings = formState.value.mapReportSettings.copy { positionPrecision = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
publishIntervalSecs = formState.value.mapReportSettings.publishIntervalSecs,
onPublishIntervalSecsChanged = {
val settings = formState.value.mapReportSettings.copy { publishIntervalSecs = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
enabled = state.connected,
)
}
}
item {
EditPasswordPreference(
title = stringResource(R.string.password),
value = formState.value.password,
maxSize = 63, // password max_size:64
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { password = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.encryption_enabled),
checked = formState.value.encryptionEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { encryptionEnabled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.json_output_enabled),
checked = formState.value.jsonEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { jsonEnabled = it } },
)
}
item { HorizontalDivider() }
item {
val defaultAddress = stringResource(R.string.default_mqtt_address)
val isDefault = formState.value.address.isEmpty() || formState.value.address.contains(defaultAddress)
val enforceTls = isDefault && formState.value.proxyToClientEnabled
SwitchPreference(
title = stringResource(R.string.tls_enabled),
checked = formState.value.tlsEnabled || enforceTls,
enabled = state.connected && !enforceTls,
onCheckedChange = { formState.value = formState.value.copy { tlsEnabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.root_topic),
value = formState.value.root,
maxSize = 31, // root max_size:32
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { root = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.proxy_to_client_enabled),
checked = formState.value.proxyToClientEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { proxyToClientEnabled = it } },
)
}
item { HorizontalDivider() }
// mqtt map reporting opt in
item { PreferenceCategory(text = stringResource(R.string.map_reporting)) }
item {
MapReportingPreference(
mapReportingEnabled = formState.value.mapReportingEnabled,
onMapReportingEnabledChanged = { formState.value = formState.value.copy { mapReportingEnabled = it } },
shouldReportLocation = formState.value.mapReportSettings.shouldReportLocation,
onShouldReportLocationChanged = {
viewModel.setShouldReportLocation(destNum, it)
val settings = formState.value.mapReportSettings.copy { this.shouldReportLocation = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
positionPrecision = formState.value.mapReportSettings.positionPrecision,
onPositionPrecisionChanged = {
val settings = formState.value.mapReportSettings.copy { positionPrecision = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
publishIntervalSecs = formState.value.mapReportSettings.publishIntervalSecs,
onPublishIntervalSecsChanged = {
val settings = formState.value.mapReportSettings.copy { publishIntervalSecs = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
enabled = state.connected,
focusManager = focusManager,
)
}
item { HorizontalDivider() }
}
}

View file

@ -20,7 +20,6 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
@ -31,11 +30,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -43,9 +41,11 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.model.util.DistanceUnit
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.precisionBitsToMeters
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import kotlin.math.roundToInt
private const val POSITION_PRECISION_MIN = 12
@ -63,7 +63,6 @@ fun MapReportingPreference(
publishIntervalSecs: Int = 3600,
onPublishIntervalSecsChanged: (Int) -> Unit = {},
enabled: Boolean,
focusManager: FocusManager,
) {
Column {
var showMapReportingWarning by rememberSaveable { mutableStateOf(mapReportingEnabled) }
@ -121,14 +120,14 @@ fun MapReportingPreference(
overflow = TextOverflow.Companion.Ellipsis,
maxLines = 1,
)
EditTextPreference(
val publishItems = remember { IntervalConfiguration.BROADCAST_MEDIUM.allowedIntervals }
DropDownPreference(
modifier = Modifier.padding(bottom = 16.dp),
title = stringResource(R.string.map_reporting_interval_seconds),
value = publishIntervalSecs,
isError = publishIntervalSecs < 3600,
items = publishItems.map { it.value to it.toDisplayString() },
selectedItem = publishIntervalSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = onPublishIntervalSecsChanged,
onItemSelected = { onPublishIntervalSecsChanged(it.toInt()) },
)
}
}
@ -139,7 +138,6 @@ fun MapReportingPreference(
@Preview(showBackground = true)
@Composable
fun MapReportingPreview() {
val focusManager = LocalFocusManager.current
MapReportingPreference(
mapReportingEnabled = true,
onMapReportingEnabledChanged = {},
@ -148,6 +146,5 @@ fun MapReportingPreview() {
positionPrecision = 5,
onPositionPrecisionChanged = {},
enabled = true,
focusManager = focusManager,
)
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -28,8 +29,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -53,37 +54,33 @@ fun NeighborInfoConfigScreen(navController: NavController, viewModel: RadioConfi
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.neighbor_info_config)) }
item {
SwitchPreference(
title = stringResource(R.string.neighbor_info_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.update_interval_seconds),
value = formState.value.updateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { updateInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.transmit_over_lora),
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
checked = formState.value.transmitOverLora,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { transmitOverLora = it } },
)
HorizontalDivider()
TitledCard(title = stringResource(R.string.neighbor_info_config)) {
SwitchPreference(
title = stringResource(R.string.neighbor_info_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.update_interval_seconds),
value = formState.value.updateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { updateInterval = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.transmit_over_lora),
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
checked = formState.value.transmitOverLora,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { transmitOverLora = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
}
}

View file

@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -47,9 +48,9 @@ import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditIPv4Preference
import org.meshtastic.core.ui.component.EditPasswordPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SimpleAlertDialog
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ConfigProtos.Config.NetworkConfig
import org.meshtastic.proto.config
@ -111,181 +112,163 @@ fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigView
},
) {
if (state.metadata?.hasWifi == true) {
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 = formState.value.wifiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wifiEnabled = it } },
)
HorizontalDivider()
}
item {
EditTextPreference(
title = stringResource(R.string.ssid),
value = formState.value.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiSsid = it } },
)
}
item {
EditPasswordPreference(
title = stringResource(R.string.password),
value = formState.value.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiPsk = it } },
)
}
item {
Button(
onClick = { zxingScan() },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = state.connected,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
TitledCard(title = stringResource(R.string.wifi_config)) {
SwitchPreference(
title = stringResource(R.string.wifi_enabled),
summary = stringResource(id = R.string.config_network_wifi_enabled_summary),
checked = formState.value.wifiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wifiEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.ssid),
value = formState.value.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiSsid = it } },
)
HorizontalDivider()
EditPasswordPreference(
title = stringResource(R.string.password),
value = formState.value.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiPsk = it } },
)
HorizontalDivider()
Button(
onClick = { zxingScan() },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = state.connected,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
}
}
}
}
if (state.metadata?.hasEthernet == true) {
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 = formState.value.ethEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ethEnabled = it } },
)
HorizontalDivider()
TitledCard(title = stringResource(R.string.ethernet_config)) {
SwitchPreference(
title = stringResource(R.string.ethernet_enabled),
summary = stringResource(id = R.string.config_network_eth_enabled_summary),
checked = formState.value.ethEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ethEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
}
if (state.metadata?.hasEthernet == true || state.metadata?.hasWifi == true) {
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 = formState.value.enabledProtocols == 1,
TitledCard(title = stringResource(R.string.udp_config)) {
SwitchPreference(
title = stringResource(R.string.udp_enabled),
summary = stringResource(id = R.string.config_network_udp_enabled_summary),
checked = formState.value.enabledProtocols == 1,
enabled = state.connected,
onCheckedChange = {
formState.value =
formState.value.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
},
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
}
item {
TitledCard(title = stringResource(R.string.advanced)) {
EditTextPreference(
title = stringResource(R.string.ntp_server),
value = formState.value.ntpServer,
maxSize = 32, // ntp_server max_size:33
enabled = state.connected,
onCheckedChange = {
formState.value =
formState.value.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
isError = formState.value.ntpServer.isEmpty(),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { ntpServer = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.rsyslog_server),
value = formState.value.rsyslogServer,
maxSize = 32, // rsyslog_server max_size:33
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { rsyslogServer = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.ipv4_mode),
enabled = state.connected,
items =
NetworkConfig.AddressMode.entries
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.addressMode,
onItemSelected = { formState.value = formState.value.copy { addressMode = it } },
)
HorizontalDivider()
EditIPv4Preference(
title = stringResource(R.string.ip),
value = formState.value.ipv4Config.ip,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { ip = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
HorizontalDivider()
EditIPv4Preference(
title = stringResource(R.string.gateway),
value = formState.value.ipv4Config.gateway,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { gateway = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
HorizontalDivider()
EditIPv4Preference(
title = stringResource(R.string.subnet),
value = formState.value.ipv4Config.subnet,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { subnet = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
HorizontalDivider()
EditIPv4Preference(
title = "DNS",
value = formState.value.ipv4Config.dns,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { dns = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item { HorizontalDivider() }
}
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
EditTextPreference(
title = stringResource(R.string.ntp_server),
value = formState.value.ntpServer,
maxSize = 32, // ntp_server max_size:33
enabled = state.connected,
isError = formState.value.ntpServer.isEmpty(),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { ntpServer = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.rsyslog_server),
value = formState.value.rsyslogServer,
maxSize = 32, // rsyslog_server max_size:33
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { rsyslogServer = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.ipv4_mode),
enabled = state.connected,
items =
NetworkConfig.AddressMode.entries
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.addressMode,
onItemSelected = { formState.value = formState.value.copy { addressMode = it } },
)
HorizontalDivider()
}
item {
EditIPv4Preference(
title = stringResource(R.string.ip),
value = formState.value.ipv4Config.ip,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { ip = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item {
EditIPv4Preference(
title = stringResource(R.string.gateway),
value = formState.value.ipv4Config.gateway,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { gateway = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item {
EditIPv4Preference(
title = stringResource(R.string.subnet),
value = formState.value.ipv4Config.subnet,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { subnet = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item {
EditIPv4Preference(
title = "DNS",
value = formState.value.ipv4Config.dns,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = formState.value.ipv4Config.copy { dns = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item { HorizontalDivider() }
}
}

View file

@ -18,20 +18,24 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -54,46 +58,43 @@ fun PaxcounterConfigScreen(navController: NavController, viewModel: RadioConfigV
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.paxcounter_config)) }
item {
SwitchPreference(
title = stringResource(R.string.paxcounter_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.update_interval_seconds),
value = formState.value.paxcounterUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { paxcounterUpdateInterval = it } },
)
}
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.wifi_rssi_threshold_defaults_to_80),
value = formState.value.wifiThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiThreshold = it } },
)
}
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.ble_rssi_threshold_defaults_to_80),
value = formState.value.bleThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { bleThreshold = it } },
)
TitledCard(title = stringResource(R.string.paxcounter_config)) {
SwitchPreference(
title = stringResource(R.string.paxcounter_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val items = remember { IntervalConfiguration.PAX_COUNTER.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.update_interval_seconds),
selectedItem = formState.value.paxcounterUpdateInterval.toLong(),
enabled = state.connected,
items = items.map { it.value to it.toDisplayString() },
onItemSelected = {
formState.value = formState.value.copy { paxcounterUpdateInterval = it.toInt() }
},
)
HorizontalDivider()
SignedIntegerEditTextPreference(
title = stringResource(R.string.wifi_rssi_threshold_defaults_to_80),
value = formState.value.wifiThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { wifiThreshold = it } },
)
HorizontalDivider()
SignedIntegerEditTextPreference(
title = stringResource(R.string.ble_rssi_threshold_defaults_to_80),
value = formState.value.bleThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { bleThreshold = it } },
)
}
}
}
}

View file

@ -22,6 +22,7 @@ import android.annotation.SuppressLint
import android.location.Location
import android.os.Build
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -47,10 +48,13 @@ import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.BitwisePreference
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.feature.settings.util.FixedUpdateIntervals
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.gpioPins
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.ConfigProtos.Config.PositionConfig
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
@ -70,7 +74,23 @@ fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigVie
time = 1, // ignore time for fixed_position
)
val positionConfig = state.radioConfig.position
val formState = rememberConfigState(initialValue = positionConfig)
val sanitizedPositionConfig =
remember(positionConfig) {
val positionItems = IntervalConfiguration.POSITION.allowedIntervals
val smartBroadcastItems = IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals
positionConfig.copy {
if (FixedUpdateIntervals.fromValue(positionBroadcastSecs.toLong()) == null) {
positionBroadcastSecs = positionItems.first().value.toInt()
}
if (FixedUpdateIntervals.fromValue(broadcastSmartMinimumIntervalSecs.toLong()) == null) {
broadcastSmartMinimumIntervalSecs = smartBroadcastItems.first().value.toInt()
}
if (FixedUpdateIntervals.fromValue(gpsUpdateInterval.toLong()) == null) {
gpsUpdateInterval = positionItems.first().value.toInt()
}
}
}
val formState = rememberConfigState(initialValue = sanitizedPositionConfig)
var locationInput by rememberSaveable { mutableStateOf(currentPosition) }
val locationPermissionState =
@ -122,182 +142,182 @@ fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigVie
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.position_packet)) }
item {
EditTextPreference(
title = stringResource(R.string.broadcast_interval),
summary = stringResource(id = R.string.config_position_broadcast_secs_summary),
value = formState.value.positionBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { positionBroadcastSecs = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.smart_position),
checked = formState.value.positionBroadcastSmartEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { positionBroadcastSmartEnabled = it } },
)
}
item { HorizontalDivider() }
if (formState.value.positionBroadcastSmartEnabled) {
item {
EditTextPreference(
title = stringResource(R.string.minimum_interval),
summary =
stringResource(id = R.string.config_position_broadcast_smart_minimum_interval_secs_summary),
value = formState.value.broadcastSmartMinimumIntervalSecs,
TitledCard(title = stringResource(R.string.position_packet)) {
val items = remember { IntervalConfiguration.BROADCAST_MEDIUM.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.broadcast_interval),
summary = stringResource(id = R.string.config_position_broadcast_secs_summary),
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
formState.value = formState.value.copy { broadcastSmartMinimumIntervalSecs = it }
items = items.map { it to it.toDisplayString() },
selectedItem =
FixedUpdateIntervals.fromValue(formState.value.positionBroadcastSecs.toLong()) ?: items.first(),
onItemSelected = {
formState.value = formState.value.copy { positionBroadcastSecs = it.value.toInt() }
},
)
}
item {
EditTextPreference(
title = stringResource(R.string.minimum_distance),
summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary),
value = formState.value.broadcastSmartMinimumDistance,
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.smart_position),
checked = formState.value.positionBroadcastSmartEnabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { broadcastSmartMinimumDistance = it } },
onCheckedChange = { formState.value = formState.value.copy { positionBroadcastSmartEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { PreferenceCategory(text = stringResource(R.string.device_gps)) }
item {
SwitchPreference(
title = stringResource(R.string.fixed_position),
checked = formState.value.fixedPosition,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { fixedPosition = it } },
)
}
item { HorizontalDivider() }
if (formState.value.fixedPosition) {
item {
EditTextPreference(
title = stringResource(R.string.latitude),
value = locationInput.latitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -90 && value <= 90.0) {
locationInput = locationInput.copy(latitude = value)
}
},
)
}
item {
EditTextPreference(
title = stringResource(R.string.longitude),
value = locationInput.longitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -180 && value <= 180.0) {
locationInput = locationInput.copy(longitude = value)
}
},
)
}
item {
EditTextPreference(
title = stringResource(R.string.altitude),
value = locationInput.altitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) },
)
}
item {
TextButton(
enabled = state.connected,
onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } },
) {
Text(text = stringResource(R.string.position_config_set_fixed_from_phone))
if (formState.value.positionBroadcastSmartEnabled) {
HorizontalDivider()
val smartItems = remember { IntervalConfiguration.SMART_BROADCAST_MINIMUM.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.minimum_interval),
summary =
stringResource(id = R.string.config_position_broadcast_smart_minimum_interval_secs_summary),
enabled = state.connected,
items = smartItems.map { it to it.toDisplayString() },
selectedItem =
FixedUpdateIntervals.fromValue(formState.value.broadcastSmartMinimumIntervalSecs.toLong())
?: smartItems.first(),
onItemSelected = {
formState.value =
formState.value.copy { broadcastSmartMinimumIntervalSecs = it.value.toInt() }
},
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.minimum_distance),
summary =
stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary),
value = formState.value.broadcastSmartMinimumDistance,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
formState.value = formState.value.copy { broadcastSmartMinimumDistance = it }
},
)
}
}
}
item {
DropDownPreference(
title = stringResource(R.string.gps_mode),
enabled = state.connected,
items =
ConfigProtos.Config.PositionConfig.GpsMode.entries
.filter { it != ConfigProtos.Config.PositionConfig.GpsMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.gpsMode,
onItemSelected = { formState.value = formState.value.copy { gpsMode = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.update_interval),
summary = stringResource(id = R.string.config_position_gps_update_interval_summary),
value = formState.value.gpsUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.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 = formState.value.positionFlags,
enabled = state.connected,
items =
ConfigProtos.Config.PositionConfig.PositionFlags.entries
.filter {
it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED
TitledCard(title = stringResource(R.string.device_gps)) {
SwitchPreference(
title = stringResource(R.string.fixed_position),
checked = formState.value.fixedPosition,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { fixedPosition = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
if (formState.value.fixedPosition) {
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.latitude),
value = locationInput.latitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -90 && value <= 90.0) {
locationInput = locationInput.copy(latitude = value)
}
},
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.longitude),
value = locationInput.longitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -180 && value <= 180.0) {
locationInput = locationInput.copy(longitude = value)
}
},
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.altitude),
value = locationInput.altitude,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) },
)
HorizontalDivider()
TextButton(
enabled = state.connected,
onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } },
) {
Text(text = stringResource(R.string.position_config_set_fixed_from_phone))
}
.map { it.number to it.name },
onItemSelected = { formState.value = formState.value.copy { positionFlags = it } },
)
} else {
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.gps_mode),
enabled = state.connected,
items =
PositionConfig.GpsMode.entries
.filter { it != PositionConfig.GpsMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.gpsMode,
onItemSelected = { formState.value = formState.value.copy { gpsMode = it } },
)
HorizontalDivider()
val items = remember { IntervalConfiguration.GPS_UPDATE.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.update_interval),
summary = stringResource(id = R.string.config_position_gps_update_interval_summary),
enabled = state.connected,
items = items.map { it to it.toDisplayString() },
selectedItem =
FixedUpdateIntervals.fromValue(formState.value.gpsUpdateInterval.toLong()) ?: items.first(),
onItemSelected = {
formState.value = formState.value.copy { gpsUpdateInterval = it.value.toInt() }
},
)
}
}
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.advanced_device_gps)) }
item {
EditTextPreference(
title = stringResource(R.string.gps_receive_gpio),
value = formState.value.rxGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { rxGpio = it } },
)
TitledCard(title = stringResource(R.string.position_flags)) {
BitwisePreference(
title = stringResource(R.string.position_flags),
summary = stringResource(id = R.string.config_position_flags_summary),
value = formState.value.positionFlags,
enabled = state.connected,
items =
PositionConfig.PositionFlags.entries
.filter {
it != PositionConfig.PositionFlags.UNSET &&
it != PositionConfig.PositionFlags.UNRECOGNIZED
}
.map { it.number to it.name },
onItemSelected = { formState.value = formState.value.copy { positionFlags = it } },
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.gps_transmit_gpio),
value = formState.value.txGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gps_en_gpio),
value = formState.value.gpsEnGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { gpsEnGpio = it } },
)
TitledCard(title = stringResource(R.string.advanced_device_gps)) {
val pins = remember { gpioPins }
DropDownPreference(
title = stringResource(R.string.gps_receive_gpio),
enabled = state.connected,
items = pins,
selectedItem = formState.value.rxGpio,
onItemSelected = { formState.value = formState.value.copy { rxGpio = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.gps_transmit_gpio),
enabled = state.connected,
items = pins,
selectedItem = formState.value.txGpio,
onItemSelected = { formState.value = formState.value.copy { txGpio = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.gps_en_gpio),
enabled = state.connected,
items = pins,
selectedItem = formState.value.gpsEnGpio,
onItemSelected = { formState.value = formState.value.copy { gpsEnGpio = it } },
)
}
}
}
}

View file

@ -18,19 +18,24 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.config
import org.meshtastic.proto.copy
@ -53,107 +58,83 @@ fun PowerConfigScreen(navController: NavController, viewModel: RadioConfigViewMo
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.power_config)) }
item {
SwitchPreference(
title = stringResource(R.string.enable_power_saving_mode),
summary = stringResource(id = R.string.config_power_is_power_saving_summary),
checked = formState.value.isPowerSaving,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isPowerSaving = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.shutdown_on_power_loss),
checked = formState.value.onBatteryShutdownAfterSecs > 0,
enabled = state.connected,
onCheckedChange = {
formState.value = formState.value.copy { onBatteryShutdownAfterSecs = if (it) 3600 else 0 }
},
)
}
if (formState.value.onBatteryShutdownAfterSecs > 0) {
item {
TitledCard(title = stringResource(R.string.power_config)) {
SwitchPreference(
title = stringResource(R.string.enable_power_saving_mode),
summary = stringResource(id = R.string.config_power_is_power_saving_summary),
checked = formState.value.isPowerSaving,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isPowerSaving = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val items = remember { IntervalConfiguration.ALL.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.shutdown_on_power_loss),
selectedItem = formState.value.onBatteryShutdownAfterSecs.toLong(),
enabled = state.connected,
items = items.map { it.value to it.toDisplayString() },
onItemSelected = {
formState.value = formState.value.copy { onBatteryShutdownAfterSecs = it.toInt() }
},
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.adc_multiplier_override),
checked = formState.value.adcMultiplierOverride > 0f,
enabled = state.connected,
onCheckedChange = {
formState.value = formState.value.copy { adcMultiplierOverride = if (it) 1.0f else 0.0f }
},
containerColor = CardDefaults.cardColors().containerColor,
)
if (formState.value.adcMultiplierOverride > 0f) {
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.adc_multiplier_override_ratio),
value = formState.value.adcMultiplierOverride,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { adcMultiplierOverride = it } },
)
}
HorizontalDivider()
val waitBluetoothItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.wait_for_bluetooth_duration_seconds),
selectedItem = formState.value.waitBluetoothSecs.toLong(),
enabled = state.connected,
items = waitBluetoothItems.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { waitBluetoothSecs = it.toInt() } },
)
HorizontalDivider()
val sdsSecsItems = remember { IntervalConfiguration.ALL.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.super_deep_sleep_duration_seconds),
selectedItem = formState.value.sdsSecs.toLong(),
onItemSelected = { formState.value = formState.value.copy { sdsSecs = it.toInt() } },
enabled = state.connected,
items = sdsSecsItems.map { it.value to it.toDisplayString() },
)
HorizontalDivider()
val minWakeItems = remember { IntervalConfiguration.NAG_TIMEOUT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.minimum_wake_time_seconds),
selectedItem = formState.value.minWakeSecs.toLong(),
enabled = state.connected,
items = minWakeItems.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { minWakeSecs = it.toInt() } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.shutdown_on_battery_delay_seconds),
value = formState.value.onBatteryShutdownAfterSecs,
title = stringResource(R.string.battery_ina_2xx_i2c_address),
value = formState.value.deviceBatteryInaAddress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { onBatteryShutdownAfterSecs = it } },
onValueChanged = { formState.value = formState.value.copy { deviceBatteryInaAddress = it } },
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.adc_multiplier_override),
checked = formState.value.adcMultiplierOverride > 0f,
enabled = state.connected,
onCheckedChange = {
formState.value = formState.value.copy { adcMultiplierOverride = if (it) 1.0f else 0.0f }
},
)
}
if (formState.value.adcMultiplierOverride > 0f) {
item {
EditTextPreference(
title = stringResource(R.string.adc_multiplier_override_ratio),
value = formState.value.adcMultiplierOverride,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { adcMultiplierOverride = it } },
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.wait_for_bluetooth_duration_seconds),
value = formState.value.waitBluetoothSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { waitBluetoothSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.super_deep_sleep_duration_seconds),
value = formState.value.sdsSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { sdsSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.minimum_wake_time_seconds),
value = formState.value.minWakeSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { minWakeSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.battery_ina_2xx_i2c_address),
value = formState.value.deviceBatteryInaAddress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { deviceBatteryInaAddress = it } },
)
}
}
}

View file

@ -17,20 +17,22 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -39,7 +41,6 @@ fun RangeTestConfigScreen(navController: NavController, viewModel: RadioConfigVi
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val rangeTestConfig = state.moduleConfig.rangeTest
val formState = rememberConfigState(initialValue = rangeTestConfig)
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.range_test),
@ -53,36 +54,33 @@ fun RangeTestConfigScreen(navController: NavController, viewModel: RadioConfigVi
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.range_test_config)) }
item {
SwitchPreference(
title = stringResource(R.string.range_test_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
TitledCard(title = stringResource(R.string.range_test_config)) {
SwitchPreference(
title = stringResource(R.string.range_test_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val rangeItems = remember { IntervalConfiguration.RANGE_TEST_SENDER.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.sender_message_interval_seconds),
selectedItem = formState.value.sender.toLong(),
enabled = state.connected,
items = rangeItems.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { sender = it.toInt() } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.save_csv_in_storage_esp32_only),
checked = formState.value.save,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { save = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.sender_message_interval_seconds),
value = formState.value.sender,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { sender = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.save_csv_in_storage_esp32_only),
checked = formState.value.save,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { save = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -28,8 +29,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditListPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -53,43 +54,39 @@ fun RemoteHardwareConfigScreen(navController: NavController, viewModel: RadioCon
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.remote_hardware_config)) }
item {
SwitchPreference(
title = stringResource(R.string.remote_hardware_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.allow_undefined_pin_access),
checked = formState.value.allowUndefinedPinAccess,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { allowUndefinedPinAccess = it } },
)
}
item { HorizontalDivider() }
item {
EditListPreference(
title = stringResource(R.string.available_pins),
list = formState.value.availablePinsList,
maxCount = 4, // available_pins max_count:4
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
formState.value =
formState.value.copy {
availablePins.clear()
availablePins.addAll(list)
}
},
)
TitledCard(title = stringResource(R.string.remote_hardware_config)) {
SwitchPreference(
title = stringResource(R.string.remote_hardware_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.allow_undefined_pin_access),
checked = formState.value.allowUndefinedPinAccess,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { allowUndefinedPinAccess = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditListPreference(
title = stringResource(R.string.available_pins),
list = formState.value.availablePinsList,
maxCount = 4, // available_pins max_count:4
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
formState.value =
formState.value.copy {
availablePins.clear()
availablePins.addAll(list)
}
},
)
}
}
}
}

View file

@ -26,6 +26,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
@ -50,8 +51,8 @@ import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.CopyIconButton
import org.meshtastic.core.ui.component.EditBase64Preference
import org.meshtastic.core.ui.component.EditListPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ConfigProtos.Config.SecurityConfig
import org.meshtastic.proto.config
@ -134,121 +135,114 @@ fun SecurityConfigScreen(navController: NavController, viewModel: RadioConfigVie
viewModel.setConfig(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 = state.connected,
readOnly = true,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
formState.value = formState.value.copy { this.publicKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = formState.value.publicKey.encodeToString()) },
)
}
item {
EditBase64Preference(
title = stringResource(R.string.private_key),
summary = stringResource(id = R.string.config_security_private_key),
value = formState.value.privateKey,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
formState.value = formState.value.copy { privateKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = formState.value.privateKey.encodeToString()) },
)
}
item {
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.regenerate_private_key),
enabled = state.connected,
icon = Icons.TwoTone.Warning,
onClick = { showKeyGenerationDialog = true },
)
}
item {
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.export_keys),
enabled = state.connected,
icon = Icons.TwoTone.Warning,
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 = formState.value.adminKeyList,
maxCount = 3,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = {
formState.value =
formState.value.copy {
adminKey.clear()
adminKey.addAll(it)
TitledCard(title = stringResource(R.string.direct_message_key)) {
EditBase64Preference(
title = stringResource(R.string.public_key),
summary = stringResource(id = R.string.config_security_public_key),
value = publicKey,
enabled = state.connected,
readOnly = true,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
formState.value = formState.value.copy { this.publicKey = it }
}
},
)
},
trailingIcon = { CopyIconButton(valueToCopy = formState.value.publicKey.encodeToString()) },
)
HorizontalDivider()
EditBase64Preference(
title = stringResource(R.string.private_key),
summary = stringResource(id = R.string.config_security_private_key),
value = formState.value.privateKey,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
formState.value = formState.value.copy { privateKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = formState.value.privateKey.encodeToString()) },
)
HorizontalDivider()
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.regenerate_private_key),
enabled = state.connected,
icon = Icons.TwoTone.Warning,
onClick = { showKeyGenerationDialog = true },
)
HorizontalDivider()
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.export_keys),
enabled = state.connected,
icon = Icons.TwoTone.Warning,
onClick = { showEditSecurityConfigDialog = true },
)
}
}
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 = formState.value.serialEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { serialEnabled = it } },
)
TitledCard(title = stringResource(R.string.admin_keys)) {
EditListPreference(
title = stringResource(R.string.admin_key),
summary = stringResource(id = R.string.config_security_admin_key),
list = formState.value.adminKeyList,
maxCount = 3,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = {
formState.value =
formState.value.copy {
adminKey.clear()
adminKey.addAll(it)
}
},
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.debug_log_api_enabled),
summary = stringResource(id = R.string.config_security_debug_log_api_enabled),
checked = formState.value.debugLogApiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { debugLogApiEnabled = it } },
)
TitledCard(title = stringResource(R.string.logs)) {
SwitchPreference(
title = stringResource(R.string.serial_console),
summary = stringResource(id = R.string.config_security_serial_enabled),
checked = formState.value.serialEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { serialEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.debug_log_api_enabled),
summary = stringResource(id = R.string.config_security_debug_log_api_enabled),
checked = formState.value.debugLogApiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { debugLogApiEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
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 = formState.value.isManaged,
enabled = state.connected && formState.value.adminKeyCount > 0,
onCheckedChange = { formState.value = formState.value.copy { isManaged = it } },
)
TitledCard(title = stringResource(R.string.administration)) {
SwitchPreference(
title = stringResource(R.string.managed_mode),
summary = stringResource(id = R.string.config_security_is_managed),
checked = formState.value.isManaged,
enabled = state.connected && formState.value.adminKeyCount > 0,
onCheckedChange = { formState.value = formState.value.copy { isManaged = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.legacy_admin_channel),
checked = formState.value.adminChannelEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { adminChannelEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.legacy_admin_channel),
checked = formState.value.adminChannelEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { adminChannelEnabled = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -29,8 +30,8 @@ import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig.SerialConfig
import org.meshtastic.proto.copy
@ -55,94 +56,78 @@ fun SerialConfigScreen(navController: NavController, viewModel: RadioConfigViewM
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.serial_config)) }
item {
SwitchPreference(
title = stringResource(R.string.serial_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
TitledCard(title = stringResource(R.string.serial_config)) {
SwitchPreference(
title = stringResource(R.string.serial_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.echo_enabled),
checked = formState.value.echo,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { echo = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = "RX",
value = formState.value.rxd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { rxd = it } },
)
HorizontalDivider()
EditTextPreference(
title = "TX",
value = formState.value.txd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txd = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.serial_baud_rate),
enabled = state.connected,
items =
SerialConfig.Serial_Baud.entries
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.baud,
onItemSelected = { formState.value = formState.value.copy { baud = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.timeout),
value = formState.value.timeout,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { timeout = it } },
)
HorizontalDivider()
DropDownPreference(
title = stringResource(R.string.serial_mode),
enabled = state.connected,
items =
SerialConfig.Serial_Mode.entries
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.override_console_serial_port),
checked = formState.value.overrideConsoleSerialPort,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { overrideConsoleSerialPort = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.echo_enabled),
checked = formState.value.echo,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { echo = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = "RX",
value = formState.value.rxd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { rxd = it } },
)
}
item {
EditTextPreference(
title = "TX",
value = formState.value.txd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txd = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.serial_baud_rate),
enabled = state.connected,
items =
SerialConfig.Serial_Baud.entries
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.baud,
onItemSelected = { formState.value = formState.value.copy { baud = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.timeout),
value = formState.value.timeout,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { timeout = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.serial_mode),
enabled = state.connected,
items =
SerialConfig.Serial_Mode.entries
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.override_console_serial_port),
checked = formState.value.overrideConsoleSerialPort,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { overrideConsoleSerialPort = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -18,6 +18,7 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -28,8 +29,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
@ -53,66 +54,56 @@ fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfi
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.store_forward_config)) }
item {
SwitchPreference(
title = stringResource(R.string.store_forward_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
TitledCard(title = stringResource(R.string.store_forward_config)) {
SwitchPreference(
title = stringResource(R.string.store_forward_enabled),
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.heartbeat),
checked = formState.value.heartbeat,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { heartbeat = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.number_of_records),
value = formState.value.records,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { records = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.history_return_max),
value = formState.value.historyReturnMax,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { historyReturnMax = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.history_return_window),
value = formState.value.historyReturnWindow,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { historyReturnWindow = it } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.server),
checked = formState.value.isServer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isServer = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.heartbeat),
checked = formState.value.heartbeat,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { heartbeat = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.number_of_records),
value = formState.value.records,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { records = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.history_return_max),
value = formState.value.historyReturnMax,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { historyReturnMax = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.history_return_window),
value = formState.value.historyReturnWindow,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { historyReturnWindow = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.server),
checked = formState.value.isServer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isServer = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -17,29 +17,35 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.IntervalConfiguration
import org.meshtastic.feature.settings.util.toDisplayString
import org.meshtastic.proto.copy
import org.meshtastic.proto.moduleConfig
private const val MIN_FW_FOR_TELEMETRY_TOGGLE = "2.7.12"
@Composable
fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val telemetryConfig = state.moduleConfig.telemetry
val formState = rememberConfigState(initialValue = telemetryConfig)
val focusManager = LocalFocusManager.current
val firmwareVersion = state.metadata?.firmwareVersion ?: "1"
RadioConfigScreenList(
title = stringResource(id = R.string.telemetry),
@ -53,116 +59,105 @@ fun TelemetryConfigScreen(navController: NavController, viewModel: RadioConfigVi
viewModel.setModuleConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.telemetry_config)) }
item {
SwitchPreference(
title = stringResource(R.string.device_telemetry_enabled),
summary = stringResource(R.string.device_telemetry_enabled_summary),
checked = formState.value.deviceTelemetryEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { deviceTelemetryEnabled = it } },
)
TitledCard(title = stringResource(R.string.telemetry_config)) {
if (DeviceVersion(firmwareVersion) >= DeviceVersion(MIN_FW_FOR_TELEMETRY_TOGGLE)) {
SwitchPreference(
title = stringResource(R.string.device_telemetry_enabled),
summary = stringResource(R.string.device_telemetry_enabled_summary),
checked = formState.value.deviceTelemetryEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { deviceTelemetryEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
}
val items = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.device_metrics_update_interval_seconds),
selectedItem = formState.value.deviceUpdateInterval.toLong(),
enabled = state.connected,
items = items.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { deviceUpdateInterval = it.toInt() } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.environment_metrics_module_enabled),
checked = formState.value.environmentMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentMeasurementEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val envItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.environment_metrics_update_interval_seconds),
selectedItem = formState.value.environmentUpdateInterval.toLong(),
enabled = state.connected,
items = envItems.map { it.value to it.toDisplayString() },
onItemSelected = {
formState.value = formState.value.copy { environmentUpdateInterval = it.toInt() }
},
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.environment_metrics_on_screen_enabled),
checked = formState.value.environmentScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentScreenEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.environment_metrics_use_fahrenheit),
checked = formState.value.environmentDisplayFahrenheit,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentDisplayFahrenheit = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.air_quality_metrics_module_enabled),
checked = formState.value.airQualityEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { airQualityEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val airItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.air_quality_metrics_update_interval_seconds),
selectedItem = formState.value.airQualityInterval.toLong(),
enabled = state.connected,
items = airItems.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { airQualityInterval = it.toInt() } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.power_metrics_module_enabled),
checked = formState.value.powerMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerMeasurementEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
val powerItems = remember { IntervalConfiguration.BROADCAST_SHORT.allowedIntervals }
DropDownPreference(
title = stringResource(R.string.power_metrics_update_interval_seconds),
selectedItem = formState.value.powerUpdateInterval.toLong(),
enabled = state.connected,
items = powerItems.map { it.value to it.toDisplayString() },
onItemSelected = { formState.value = formState.value.copy { powerUpdateInterval = it.toInt() } },
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.power_metrics_on_screen_enabled),
checked = formState.value.powerScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerScreenEnabled = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item {
EditTextPreference(
title = stringResource(R.string.device_metrics_update_interval_seconds),
value = formState.value.deviceUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { deviceUpdateInterval = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.environment_metrics_update_interval_seconds),
value = formState.value.environmentUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { environmentUpdateInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_module_enabled),
checked = formState.value.environmentMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_on_screen_enabled),
checked = formState.value.environmentScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentScreenEnabled = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_use_fahrenheit),
checked = formState.value.environmentDisplayFahrenheit,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentDisplayFahrenheit = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.air_quality_metrics_module_enabled),
checked = formState.value.airQualityEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { airQualityEnabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.air_quality_metrics_update_interval_seconds),
value = formState.value.airQualityInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { airQualityInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.power_metrics_module_enabled),
checked = formState.value.powerMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.power_metrics_update_interval_seconds),
value = formState.value.powerUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { powerUpdateInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.power_metrics_on_screen_enabled),
checked = formState.value.powerScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerScreenEnabled = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -19,6 +19,7 @@ package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -33,9 +34,9 @@ import org.meshtastic.core.database.model.isUnmessageableRole
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.RegularPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.copy
@ -60,73 +61,60 @@ fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = viewModel::setOwner,
) {
item { PreferenceCategory(text = stringResource(R.string.user_config)) }
item {
RegularPreference(title = stringResource(R.string.node_id), subtitle = formState.value.id, onClick = {})
TitledCard(title = stringResource(R.string.user_config)) {
RegularPreference(title = stringResource(R.string.node_id), subtitle = formState.value.id, onClick = {})
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.long_name),
value = formState.value.longName,
maxSize = 39, // long_name max_size:40
enabled = state.connected,
isError = !validLongName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { longName = it } },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(R.string.short_name),
value = formState.value.shortName,
maxSize = 4, // short_name max_size:5
enabled = state.connected,
isError = !validShortName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { shortName = it } },
)
HorizontalDivider()
RegularPreference(
title = stringResource(R.string.hardware_model),
subtitle = formState.value.hwModel.name,
onClick = {},
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.unmessageable),
summary = stringResource(R.string.unmonitored_or_infrastructure),
checked =
formState.value.isUnmessagable ||
(firmwareVersion < DeviceVersion("2.6.9") && formState.value.role.isUnmessageableRole()),
enabled = formState.value.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"),
onCheckedChange = { formState.value = formState.value.copy { isUnmessagable = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(R.string.licensed_amateur_radio),
summary = stringResource(R.string.licensed_amateur_radio_text),
checked = formState.value.isLicensed,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isLicensed = it } },
containerColor = CardDefaults.cardColors().containerColor,
)
}
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.long_name),
value = formState.value.longName,
maxSize = 39, // long_name max_size:40
enabled = state.connected,
isError = !validLongName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { longName = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.short_name),
value = formState.value.shortName,
maxSize = 4, // short_name max_size:5
enabled = state.connected,
isError = !validShortName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { shortName = it } },
)
}
item {
RegularPreference(
title = stringResource(R.string.hardware_model),
subtitle = formState.value.hwModel.name,
onClick = {},
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.unmessageable),
summary = stringResource(R.string.unmonitored_or_infrastructure),
checked =
formState.value.isUnmessagable ||
(firmwareVersion < DeviceVersion("2.6.9") && formState.value.role.isUnmessageableRole()),
enabled = formState.value.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"),
onCheckedChange = { formState.value = formState.value.copy { isUnmessagable = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.licensed_amateur_radio),
summary = stringResource(R.string.licensed_amateur_radio_text),
checked = formState.value.isLicensed,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isLicensed = it } },
)
}
item { HorizontalDivider() }
}
}

View file

@ -0,0 +1,372 @@
/*
* 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/>.
*/
@file:Suppress("MagicNumber")
package org.meshtastic.feature.settings.util
import java.util.concurrent.TimeUnit
/**
* Defines a set of fixed time intervals in seconds, commonly used for configuration settings.
*
* @param value The interval duration in seconds.
*/
enum class FixedUpdateIntervals(val value: Long) {
UNSET(0L),
ONE_SECOND(1L),
TWO_SECONDS(2L),
THREE_SECONDS(3L),
FOUR_SECONDS(4L),
FIVE_SECONDS(5L),
TEN_SECONDS(10L),
FIFTEEN_SECONDS(15L),
TWENTY_SECONDS(20L),
THIRTY_SECONDS(30L),
FORTY_FIVE_SECONDS(45L),
ONE_MINUTE(TimeUnit.MINUTES.toSeconds(1)),
TWO_MINUTES(TimeUnit.MINUTES.toSeconds(2)),
FIVE_MINUTES(TimeUnit.MINUTES.toSeconds(5)),
TEN_MINUTES(TimeUnit.MINUTES.toSeconds(10)),
FIFTEEN_MINUTES(TimeUnit.MINUTES.toSeconds(15)),
THIRTY_MINUTES(TimeUnit.MINUTES.toSeconds(30)),
ONE_HOUR(TimeUnit.HOURS.toSeconds(1)),
TWO_HOURS(TimeUnit.HOURS.toSeconds(2)),
THREE_HOURS(TimeUnit.HOURS.toSeconds(3)),
FOUR_HOURS(TimeUnit.HOURS.toSeconds(4)),
FIVE_HOURS(TimeUnit.HOURS.toSeconds(5)),
SIX_HOURS(TimeUnit.HOURS.toSeconds(6)),
TWELVE_HOURS(TimeUnit.HOURS.toSeconds(12)),
EIGHTEEN_HOURS(TimeUnit.HOURS.toSeconds(18)),
TWENTY_FOUR_HOURS(TimeUnit.HOURS.toSeconds(24)),
THIRTY_SIX_HOURS(TimeUnit.HOURS.toSeconds(36)),
FORTY_EIGHT_HOURS(TimeUnit.HOURS.toSeconds(48)),
SEVENTY_TWO_HOURS(TimeUnit.HOURS.toSeconds(72)),
ALWAYS_ON(Int.MAX_VALUE.toLong()),
;
companion object {
/**
* Finds a [FixedUpdateIntervals] that matches the given value.
*
* @return The corresponding [FixedUpdateIntervals] or null if no match is found.
*/
fun fromValue(value: Long): FixedUpdateIntervals? = entries.find { it.value == value }
}
}
/**
* Represents a specific configuration context that determines a subset of allowed update intervals. This is used to
* filter the available [FixedUpdateIntervals] for a particular setting.
*/
enum class IntervalConfiguration {
ALL,
BROADCAST_SHORT,
BROADCAST_MEDIUM,
BROADCAST_LONG,
NODE_INFO_BROADCAST,
DETECTION_SENSOR_MINIMUM,
DETECTION_SENSOR_STATE,
NAG_TIMEOUT,
OUTPUT,
PAX_COUNTER,
POSITION,
POSITION_BROADCAST,
GPS_UPDATE,
RANGE_TEST_SENDER,
SMART_BROADCAST_MINIMUM,
DISPLAY_SCREEN_ON,
DISPLAY_CAROUSEL,
;
/** A list of [FixedUpdateIntervals] that are permissible for this configuration. */
val allowedIntervals: List<FixedUpdateIntervals> by lazy {
when (this) {
ALL -> FixedUpdateIntervals.entries
BROADCAST_SHORT ->
listOf(
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
BROADCAST_MEDIUM ->
listOf(
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
BROADCAST_LONG ->
listOf(
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
NODE_INFO_BROADCAST ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
OUTPUT ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.ONE_SECOND,
FixedUpdateIntervals.TWO_SECONDS,
FixedUpdateIntervals.THREE_SECONDS,
FixedUpdateIntervals.FOUR_SECONDS,
FixedUpdateIntervals.FIVE_SECONDS,
FixedUpdateIntervals.TEN_SECONDS,
)
DETECTION_SENSOR_MINIMUM ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.TWO_MINUTES,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
DETECTION_SENSOR_STATE ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
NAG_TIMEOUT ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.ONE_SECOND,
FixedUpdateIntervals.FIVE_SECONDS,
FixedUpdateIntervals.TEN_SECONDS,
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
)
PAX_COUNTER ->
listOf(
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
POSITION ->
listOf(
FixedUpdateIntervals.ONE_SECOND,
FixedUpdateIntervals.TWO_SECONDS,
FixedUpdateIntervals.FIVE_SECONDS,
FixedUpdateIntervals.TEN_SECONDS,
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.TWENTY_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.FORTY_FIVE_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.TWO_MINUTES,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
)
POSITION_BROADCAST ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.TWO_HOURS,
FixedUpdateIntervals.THREE_HOURS,
FixedUpdateIntervals.FOUR_HOURS,
FixedUpdateIntervals.FIVE_HOURS,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.EIGHTEEN_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
FixedUpdateIntervals.THIRTY_SIX_HOURS,
FixedUpdateIntervals.FORTY_EIGHT_HOURS,
FixedUpdateIntervals.SEVENTY_TWO_HOURS,
)
GPS_UPDATE ->
listOf(
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.TWO_MINUTES,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.SIX_HOURS,
FixedUpdateIntervals.TWELVE_HOURS,
FixedUpdateIntervals.TWENTY_FOUR_HOURS,
)
RANGE_TEST_SENDER ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.FORTY_FIVE_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
)
SMART_BROADCAST_MINIMUM ->
listOf(
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.FORTY_FIVE_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
)
DISPLAY_SCREEN_ON ->
listOf(
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
FixedUpdateIntervals.THIRTY_MINUTES,
FixedUpdateIntervals.ONE_HOUR,
FixedUpdateIntervals.ALWAYS_ON,
)
DISPLAY_CAROUSEL ->
listOf(
FixedUpdateIntervals.UNSET,
FixedUpdateIntervals.FIFTEEN_SECONDS,
FixedUpdateIntervals.THIRTY_SECONDS,
FixedUpdateIntervals.ONE_MINUTE,
FixedUpdateIntervals.FIVE_MINUTES,
FixedUpdateIntervals.TEN_MINUTES,
FixedUpdateIntervals.FIFTEEN_MINUTES,
)
}
}
}
/**
* Represents an update interval, which can be either a predefined fixed value or a custom manual value in seconds. This
* is a type-safe representation for settings that involve time durations.
*/
sealed class UpdateInterval {
/** The duration of the interval in seconds. */
abstract val value: Long
/** A unique, stable identifier for this interval, suitable for use in Compose keys. */
val id: String
get() =
when (this) {
is Fixed -> "fixed_$value"
is Manual -> "manual_$value"
}
/** A predefined, fixed interval. */
data class Fixed(val interval: FixedUpdateIntervals) : UpdateInterval() {
override val value: Long = interval.value
}
/** A user-defined interval, specified in seconds. */
data class Manual(override val value: Long) : UpdateInterval()
companion object {
/**
* Creates an [UpdateInterval] from a raw Long value in seconds. If the value matches a predefined
* [FixedUpdateIntervals], a [Fixed] instance is returned. Otherwise, a [Manual] instance is returned.
*
* @param value The interval duration in seconds.
*/
fun fromValue(value: Long): UpdateInterval =
FixedUpdateIntervals.fromValue(value)?.let { Fixed(it) } ?: Manual(value)
}
}

View file

@ -0,0 +1,24 @@
/*
* 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 org.meshtastic.feature.settings.util
fun FixedUpdateIntervals.toDisplayString(): String = if (this == FixedUpdateIntervals.UNSET) {
"Never"
} else {
name.split('_').joinToString(" ") { word -> word.lowercase().replaceFirstChar { it.uppercase() } }
}

View file

@ -0,0 +1,21 @@
/*
* 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 org.meshtastic.feature.settings.util
val gpioPins = (0..48).map { it to "Pin $it" }
val hopLimits = (0..7).map { it to it.toString() }