From c6be5be72faba5edf31fdd56d83e0cd58b9643f5 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:03:06 -0500 Subject: [PATCH] feat(settings): replace interval inputs with dropdowns (#3352) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../core/ui/component/DropDownPreference.kt | 82 ++-- .../core/ui/component/EditTextPreference.kt | 22 +- .../core/ui/component/SliderPreference.kt | 112 ++++++ .../AmbientLightingConfigItemList.kt | 89 ++--- .../radio/component/AudioConfigItemList.kt | 132 +++---- .../component/BluetoothConfigItemList.kt | 72 ++-- .../component/CannedMessageConfigItemList.kt | 252 ++++++------ .../DetectionSensorConfigItemList.kt | 170 ++++---- .../radio/component/DeviceConfigItemList.kt | 189 ++++----- .../radio/component/DisplayConfigItemList.kt | 257 ++++++------ .../ExternalNotificationConfigItemList.kt | 303 +++++++------- .../radio/component/LoRaConfigItemList.kt | 78 ++-- .../radio/component/MQTTConfigItemList.kt | 247 ++++++------ .../radio/component/MapReportingPreference.kt | 21 +- .../component/NeighborInfoConfigItemList.kt | 59 ++- .../radio/component/NetworkConfigItemList.kt | 303 +++++++------- .../component/PaxcounterConfigItemList.kt | 83 ++-- .../radio/component/PositionConfigItemList.kt | 348 ++++++++-------- .../radio/component/PowerConfigItemList.kt | 173 ++++---- .../component/RangeTestConfigItemList.kt | 66 ++-- .../component/RemoteHardwareConfigItemList.kt | 71 ++-- .../radio/component/SecurityConfigItemList.kt | 208 +++++----- .../radio/component/SerialConfigItemList.kt | 161 ++++---- .../component/StoreForwardConfigItemList.kt | 111 +++--- .../component/TelemetryConfigItemList.kt | 223 +++++------ .../radio/component/UserConfigItemList.kt | 122 +++--- .../settings/util/FixedUpdateIntervals.kt | 372 ++++++++++++++++++ .../feature/settings/util/Formatting.kt | 24 ++ .../settings/util/SettingsIntervals.kt | 21 + 29 files changed, 2343 insertions(+), 2028 deletions(-) create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt create mode 100644 feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt create mode 100644 feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt create mode 100644 feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt index 35d262991..a3e30c60a 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt @@ -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 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 + }, + ) + } + } } } } diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt index 954ef818d..0351d3227 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt @@ -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( diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt new file mode 100644 index 000000000..47626c562 --- /dev/null +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt @@ -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 . + */ + +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 SliderPreference( + title: String, + enabled: Boolean, + items: List>, + 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 = {}, + ) + } +} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt index ed412c233..19a3cfd7b 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AmbientLightingConfigItemList.kt @@ -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 } }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt index aa0e13398..9ff47462a 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/AudioConfigItemList.kt @@ -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 } }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt index 90ae67878..a0709527d 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/BluetoothConfigItemList.kt @@ -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 } + } + }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt index 45712c48c..8aebac4eb 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/CannedMessageConfigItemList.kt @@ -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 }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt index 17c67aa5c..de62c07e3 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DetectionSensorConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt index ac21aefcd..95c5b15bc 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigItemList.kt @@ -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 } }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt index 9023d3a76..549a6a496 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/DisplayConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt index a5be1f9fa..c9829c0cc 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/ExternalNotificationConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt index c96d59e64..26d1f358e 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/LoRaConfigItemList.kt @@ -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, ) } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index fa72159f3..9428f26c1 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt index 8b3d65615..caa12f2b9 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MapReportingPreference.kt @@ -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, ) } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt index a5a8722ef..06891aa37 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NeighborInfoConfigItemList.kt @@ -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, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt index 32eb46a35..627609117 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/NetworkConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt index 9b04cc5b8..0dbfcfacc 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PaxcounterConfigItemList.kt @@ -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 } }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt index 8b70d3f51..130be2b24 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PositionConfigItemList.kt @@ -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 } }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt index af0c71428..d0aacf30a 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/PowerConfigItemList.kt @@ -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 } }, - ) - } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt index 305341667..88bd21e91 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RangeTestConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt index 95b410c7d..ba5359d07 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RemoteHardwareConfigItemList.kt @@ -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) + } + }, + ) + } } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt index 00fb43249..e9e223a3e 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SecurityConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt index c4d3ad589..0fad72f53 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/SerialConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt index bb0c4011d..5f96f995b 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/StoreForwardConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt index 76a056a8c..896193598 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/TelemetryConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt index 895dd964b..41e14d26d 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/UserConfigItemList.kt @@ -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() } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt new file mode 100644 index 000000000..86d21c034 --- /dev/null +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/FixedUpdateIntervals.kt @@ -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 . + */ + +@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 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) + } +} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt new file mode 100644 index 000000000..0737f247b --- /dev/null +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/Formatting.kt @@ -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 . + */ + +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() } } +} diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt new file mode 100644 index 000000000..779e8b878 --- /dev/null +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/util/SettingsIntervals.kt @@ -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 . + */ + +package org.meshtastic.feature.settings.util + +val gpioPins = (0..48).map { it to "Pin $it" } +val hopLimits = (0..7).map { it to it.toString() }