From 3951ebb37555a7030b1b6bd53677152323c1f99a Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Sun, 28 Sep 2025 12:52:42 -0400
Subject: [PATCH] Spruce up `LoRaConfigScreen` (#3224)
---
app/detekt-baseline.xml | 4 +-
.../common/components/DropDownPreference.kt | 22 +-
.../ui/common/components/PreferenceDivider.kt | 29 ++
.../radio/components/LoRaConfigItemList.kt | 355 +++++++++---------
.../radio/components/RadioConfigScreenList.kt | 36 +-
core/model/build.gradle.kts | 1 +
.../meshtastic/core/model/ChannelOption.kt | 22 +-
core/strings/src/main/res/values/strings.xml | 13 +-
core/ui/detekt-baseline.xml | 3 +-
.../core/ui/component/EditTextPreference.kt | 29 +-
.../core/ui/component/PreferenceFooter.kt | 56 +--
gradle/libs.versions.toml | 1 +
12 files changed, 332 insertions(+), 239 deletions(-)
create mode 100644 app/src/main/java/com/geeksville/mesh/ui/common/components/PreferenceDivider.kt
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 10c12068b..8591b9713 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -194,6 +194,7 @@
ModifierMissing:EmptyStateContent.kt$EmptyStateContent
ModifierMissing:EnvironmentMetrics.kt$EnvironmentMetricsScreen
ModifierMissing:HostMetricsLog.kt$HostMetricsLogScreen
+ ModifierMissing:LoRaConfigItemList.kt$LoRaConfigScreen
ModifierMissing:Main.kt$MainScreen
ModifierMissing:MapReportingPreference.kt$MapReportingPreference
ModifierMissing:MessageActions.kt$MessageStatusButton
@@ -210,6 +211,7 @@
ModifierMissing:PositionLog.kt$PositionItem
ModifierMissing:PositionLog.kt$PositionLogScreen
ModifierMissing:PowerMetrics.kt$PowerMetricsScreen
+ ModifierMissing:PreferenceDivider.kt$PreferenceDivider
ModifierMissing:RadioConfig.kt$RadioConfigItemList
ModifierMissing:RadioConfigScreenList.kt$RadioConfigScreenList
ModifierMissing:Reaction.kt$ReactionDialog
@@ -393,8 +395,8 @@
TopLevelPropertyNaming:Constants.kt$const val prefix = "com.geeksville.mesh"
UnusedParameter:ChannelSettingsItemList.kt$onBack: () -> Unit
UnusedParameter:ChannelSettingsItemList.kt$title: String
+ UnusedParameter:DropDownPreference.kt$modifier: Modifier = Modifier
UtilityClassWithPublicConstructor:NetworkRepositoryModule.kt$NetworkRepositoryModule
- ViewModelForwarding:Main.kt$MainAppBar( viewModel = uIViewModel, navController = navController, onAction = { action -> when (action) { is NodeMenuAction.MoreDetails -> { navController.navigate( NodesRoutes.NodeDetailGraph(action.node.num), { launchSingleTop = true restoreState = true }, ) } is NodeMenuAction.Share -> sharedContact = action.node else -> {} } }, )
ViewModelForwarding:Main.kt$ScannedQrCodeDialog(uIViewModel, newChannelSet)
ViewModelForwarding:Main.kt$VersionChecks(uIViewModel)
ViewModelInjection:DebugSearch.kt$viewModel
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt
index 0cb517b22..36912e98c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt
@@ -32,6 +32,7 @@ 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
@@ -90,18 +91,29 @@ fun DropDownPreference(
expanded = !expanded
}
},
- modifier = modifier.padding(vertical = 8.dp),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
- value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
+ value = "",
onValueChange = {},
- label = { Text(title) },
+ prefix = { Text(title) },
+ suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
- colors = ExposedDropdownMenuDefaults.textFieldColors(),
+ colors =
+ ExposedDropdownMenuDefaults.outlinedTextFieldColors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent,
+ disabledBorderColor = Color.Transparent,
+ errorBorderColor = Color.Transparent,
+ ),
enabled = enabled,
- supportingText = { if (summary != null) Text(text = summary) },
+ supportingText =
+ if (summary != null) {
+ { Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
+ } else {
+ null
+ },
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/PreferenceDivider.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/PreferenceDivider.kt
new file mode 100644
index 000000000..9914b67a7
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/PreferenceDivider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 com.geeksville.mesh.ui.common.components
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun PreferenceDivider() {
+ HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
index 7d195ab81..02a2822a3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
@@ -17,31 +17,36 @@
package com.geeksville.mesh.ui.settings.radio.components
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
-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.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
-import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
+import com.geeksville.mesh.ui.common.components.PreferenceDivider
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.model.Channel
+import org.meshtastic.core.model.ChannelOption
import org.meshtastic.core.model.RegionInfo
import org.meshtastic.core.model.numChannels
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.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
+import org.meshtastic.core.ui.component.TitledCard
@Composable
fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
@@ -65,184 +70,182 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
viewModel.setConfig(config)
},
) {
- item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
- DropDownPreference(
- title = stringResource(R.string.region_frequency_plan),
- summary = stringResource(id = R.string.config_lora_region_summary),
- enabled = state.connected,
- items = RegionInfo.entries.map { it.regionCode to it.description },
- selectedItem = formState.value.region,
- onItemSelected = { formState.value = formState.value.copy { region = it } },
- )
- }
- item { HorizontalDivider() }
- item {
- SwitchPreference(
- title = stringResource(R.string.use_modem_preset),
- checked = formState.value.usePreset,
- enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
- )
- }
- item { HorizontalDivider() }
-
- if (formState.value.usePreset) {
- item {
+ TitledCard(title = stringResource(R.string.options)) {
DropDownPreference(
- title = stringResource(R.string.modem_preset),
- summary = stringResource(id = R.string.config_lora_modem_preset_summary),
- enabled = state.connected && formState.value.usePreset,
- items =
- LoRaConfig.ModemPreset.entries
- .filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
- .map { it to it.name },
- selectedItem = formState.value.modemPreset,
- onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
- )
- }
- item { HorizontalDivider() }
- } else {
- item {
- EditTextPreference(
- title = stringResource(R.string.bandwidth),
- value = formState.value.bandwidth,
- enabled = state.connected && !formState.value.usePreset,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
- )
- }
-
- item {
- EditTextPreference(
- title = stringResource(R.string.spread_factor),
- value = formState.value.spreadFactor,
- enabled = state.connected && !formState.value.usePreset,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
- )
- }
-
- item {
- EditTextPreference(
- title = stringResource(R.string.coding_rate),
- value = formState.value.codingRate,
- enabled = state.connected && !formState.value.usePreset,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
- )
- }
- }
- item { PreferenceCategory(text = stringResource(R.string.advanced)) }
-
- item {
- SwitchPreference(
- title = stringResource(R.string.ignore_mqtt),
- checked = formState.value.ignoreMqtt,
- enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
- )
- }
- item { HorizontalDivider() }
- item {
- SwitchPreference(
- title = stringResource(R.string.ok_to_mqtt),
- checked = formState.value.configOkToMqtt,
- enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
- )
- }
- item { HorizontalDivider() }
-
- item {
- SwitchPreference(
- title = stringResource(R.string.tx_enabled),
- checked = formState.value.txEnabled,
- enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
- )
- }
- item { HorizontalDivider() }
- item {
- EditTextPreference(
- title = stringResource(R.string.hop_limit),
- summary = stringResource(id = R.string.config_lora_hop_limit_summary),
- value = formState.value.hopLimit,
- enabled = state.connected,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
- )
- }
- item { HorizontalDivider() }
-
- item {
- var isFocused by remember { mutableStateOf(false) }
- EditTextPreference(
- title = stringResource(R.string.frequency_slot),
- summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
- value =
- if (isFocused || formState.value.channelNum != 0) {
- formState.value.channelNum
- } else {
- primaryChannel.channelNum
- },
- enabled = state.connected,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onFocusChanged = { isFocused = it.isFocused },
- onValueChanged = {
- if (it <= formState.value.numChannels) { // total num of LoRa channels
- formState.value = formState.value.copy { channelNum = it }
- }
- },
- )
- }
- item { HorizontalDivider() }
- item {
- SwitchPreference(
- title = stringResource(R.string.sx126x_rx_boosted_gain),
- checked = formState.value.sx126XRxBoostedGain,
- enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
- )
- }
- item { HorizontalDivider() }
- item {
- var isFocused by remember { mutableStateOf(false) }
- EditTextPreference(
- title = stringResource(R.string.override_frequency_mhz),
- value =
- if (isFocused || formState.value.overrideFrequency != 0f) {
- formState.value.overrideFrequency
- } else {
- primaryChannel.radioFreq
- },
- enabled = state.connected,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onFocusChanged = { isFocused = it.isFocused },
- onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
- )
- }
- item { HorizontalDivider() }
- item {
- SignedIntegerEditTextPreference(
- title = stringResource(R.string.tx_power_dbm),
- value = formState.value.txPower,
- enabled = state.connected,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = { formState.value = formState.value.copy { txPower = it } },
- )
- }
-
- if (viewModel.hasPaFan) {
- item {
- SwitchPreference(
- title = stringResource(R.string.pa_fan_disabled),
- checked = formState.value.paFanDisabled,
+ title = stringResource(R.string.region_frequency_plan),
+ summary = stringResource(id = R.string.config_lora_region_summary),
enabled = state.connected,
- onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
+ items = RegionInfo.entries.map { it.regionCode to it.description },
+ selectedItem = formState.value.region,
+ onItemSelected = { formState.value = formState.value.copy { region = it } },
)
+
+ PreferenceDivider()
+
+ 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,
+ )
+
+ PreferenceDivider()
+
+ if (formState.value.usePreset) {
+ DropDownPreference(
+ title = stringResource(R.string.modem_preset),
+ summary = stringResource(id = R.string.config_lora_modem_preset_summary),
+ enabled = state.connected && formState.value.usePreset,
+ items = ChannelOption.entries.map { it.modemPreset to stringResource(it.labelRes) },
+ selectedItem = formState.value.modemPreset,
+ onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
+ )
+ } else {
+ EditTextPreference(
+ title = stringResource(R.string.bandwidth),
+ value = formState.value.bandwidth,
+ enabled = state.connected && !formState.value.usePreset,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
+ )
+
+ PreferenceDivider()
+
+ EditTextPreference(
+ title = stringResource(R.string.spread_factor),
+ value = formState.value.spreadFactor,
+ enabled = state.connected && !formState.value.usePreset,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
+ )
+
+ PreferenceDivider()
+
+ EditTextPreference(
+ title = stringResource(R.string.coding_rate),
+ value = formState.value.codingRate,
+ enabled = state.connected && !formState.value.usePreset,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
+ )
+ }
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(16.dp)) }
+
+ item {
+ TitledCard(title = stringResource(R.string.advanced)) {
+ SwitchPreference(
+ title = stringResource(R.string.ignore_mqtt),
+ checked = formState.value.ignoreMqtt,
+ enabled = state.connected,
+ onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
+ containerColor = Color.Transparent,
+ )
+
+ PreferenceDivider()
+
+ 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,
+ )
+
+ PreferenceDivider()
+
+ 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,
+ )
+
+ PreferenceDivider()
+
+ EditTextPreference(
+ title = stringResource(R.string.hop_limit),
+ summary = stringResource(id = R.string.config_lora_hop_limit_summary),
+ value = formState.value.hopLimit,
+ enabled = state.connected,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
+ )
+
+ PreferenceDivider()
+
+ var isFocusedSlot by remember { mutableStateOf(false) }
+ EditTextPreference(
+ title = stringResource(R.string.frequency_slot),
+ summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
+ value =
+ if (isFocusedSlot || formState.value.channelNum != 0) {
+ formState.value.channelNum
+ } else {
+ primaryChannel.channelNum
+ },
+ enabled = state.connected,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onFocusChanged = { isFocusedSlot = it.isFocused },
+ onValueChanged = {
+ if (it <= formState.value.numChannels) { // total num of LoRa channels
+ formState.value = formState.value.copy { channelNum = it }
+ }
+ },
+ )
+
+ PreferenceDivider()
+
+ 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,
+ )
+
+ PreferenceDivider()
+
+ var isFocusedOverride by remember { mutableStateOf(false) }
+ EditTextPreference(
+ title = stringResource(R.string.override_frequency_mhz),
+ value =
+ if (isFocusedOverride || formState.value.overrideFrequency != 0f) {
+ formState.value.overrideFrequency
+ } else {
+ primaryChannel.radioFreq
+ },
+ enabled = state.connected,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onFocusChanged = { isFocusedOverride = it.isFocused },
+ onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
+ )
+
+ PreferenceDivider()
+
+ SignedIntegerEditTextPreference(
+ title = stringResource(R.string.tx_power_dbm),
+ value = formState.value.txPower,
+ enabled = state.connected,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = { formState.value = formState.value.copy { txPower = it } },
+ )
+
+ if (viewModel.hasPaFan) {
+ 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,
+ )
+ }
}
- item { HorizontalDivider() }
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt
index 663629128..d959d1e30 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/RadioConfigScreenList.kt
@@ -17,6 +17,8 @@
package com.geeksville.mesh.ui.settings.radio.components
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -25,9 +27,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.settings.radio.ResponseState
import com.google.protobuf.MessageLite
+import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.PreferenceFooter
@Composable
@@ -61,21 +66,24 @@ fun RadioConfigScreenList(
)
},
) { innerPadding ->
- LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
- content()
- item {
- PreferenceFooter(
- enabled = enabled && configState.isDirty,
- onCancelClicked = {
- focusManager.clearFocus()
- configState.reset()
- },
- onSaveClicked = {
- focusManager.clearFocus()
- onSave(configState.value)
- },
- )
+ Column(modifier = Modifier.padding(innerPadding)) {
+ LazyColumn(modifier = Modifier.fillMaxSize().weight(1f), contentPadding = PaddingValues(16.dp)) {
+ content()
}
+
+ PreferenceFooter(
+ enabled = enabled && configState.isDirty,
+ negativeText = stringResource(R.string.discard_changes),
+ onNegativeClicked = {
+ focusManager.clearFocus()
+ configState.reset()
+ },
+ positiveText = stringResource(R.string.save_changes),
+ onPositiveClicked = {
+ focusManager.clearFocus()
+ onSave(configState.value)
+ },
+ )
}
}
}
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
index ebb1f23be..983fe7c4c 100644
--- a/core/model/build.gradle.kts
+++ b/core/model/build.gradle.kts
@@ -33,6 +33,7 @@ android {
dependencies {
implementation(projects.core.proto)
implementation(projects.core.strings)
+ implementation(libs.annotation)
implementation(libs.timber)
implementation(libs.zxing.android.embedded) { isTransitive = false }
implementation(libs.zxing.core)
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
index 10e345f17..01afb4684 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
@@ -17,9 +17,11 @@
package org.meshtastic.core.model
+import androidx.annotation.StringRes
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.RegionCode
+import org.meshtastic.core.strings.R
import kotlin.math.floor
/** hash a string into an integer using the djb2 algorithm by Dan Bernstein http://www.cse.yorku.ca/~oz/hash.html */
@@ -294,14 +296,14 @@ enum class RegionInfo(
}
}
-enum class ChannelOption(val modemPreset: ModemPreset, val bandwidth: Float) {
- SHORT_TURBO(ModemPreset.SHORT_TURBO, bandwidth = .500f),
- SHORT_FAST(ModemPreset.SHORT_FAST, .250f),
- SHORT_SLOW(ModemPreset.SHORT_SLOW, .250f),
- MEDIUM_FAST(ModemPreset.MEDIUM_FAST, .250f),
- MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, .250f),
- LONG_FAST(ModemPreset.LONG_FAST, .250f),
- LONG_MODERATE(ModemPreset.LONG_MODERATE, .125f),
- LONG_SLOW(ModemPreset.LONG_SLOW, .125f),
- VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, .0625f),
+enum class ChannelOption(val modemPreset: ModemPreset, @StringRes val labelRes: Int, val bandwidth: Float) {
+ VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, R.string.label_very_long_slow, .0625f),
+ LONG_FAST(ModemPreset.LONG_FAST, R.string.label_long_fast, .250f),
+ LONG_MODERATE(ModemPreset.LONG_MODERATE, R.string.label_long_moderate, .125f),
+ LONG_SLOW(ModemPreset.LONG_SLOW, R.string.label_long_slow, .125f),
+ MEDIUM_FAST(ModemPreset.MEDIUM_FAST, R.string.label_medium_fast, .250f),
+ MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, R.string.label_medium_slow, .250f),
+ SHORT_TURBO(ModemPreset.SHORT_TURBO, R.string.label_short_turbo, bandwidth = .500f),
+ SHORT_FAST(ModemPreset.SHORT_FAST, R.string.label_short_fast, .250f),
+ SHORT_SLOW(ModemPreset.SHORT_SLOW, R.string.label_short_slow, .250f),
}
diff --git a/core/strings/src/main/res/values/strings.xml b/core/strings/src/main/res/values/strings.xml
index d018563a0..efa195dc3 100644
--- a/core/strings/src/main/res/values/strings.xml
+++ b/core/strings/src/main/res/values/strings.xml
@@ -125,6 +125,16 @@
Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.
Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name and will change from the default public slot. Change back to the public default slot if private primary and public secondary channels are configured.
+ Very Long Range - Slow
+ Long Range - Fast
+ Long Range - Moderate
+ Long Range - Slow
+ Medium Range - Fast
+ Medium Range - Slow
+ Short Range - Turbo
+ Short Range - Fast
+ Short Range - Slow
+
Enabling WiFi will disable the bluetooth connection to the app.
Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.
Enable broadcasting packets via UDP over the local network.
@@ -174,7 +184,8 @@
Allow analytics and crash reporting.
Accept
Cancel
- Clear changes
+ Discard changes
+ Save
New Channel URL received
Meshtastic needs location permissions enabled to find new devices via Bluetooth. You can disable when not in use.
Report Bug
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index 542ca69e7..102a440de 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -8,6 +8,7 @@
ComposableParamOrder:MaterialBatteryInfo.kt$MaterialBatteryInfo
ComposableParamOrder:SwitchPreference.kt$SwitchPreference
ContentSlotReused:AdaptiveTwoPane.kt$second
+ LongMethod:EditTextPreference.kt$@Composable fun EditTextPreference( title: String, value: String, enabled: Boolean, isError: Boolean, keyboardOptions: KeyboardOptions, keyboardActions: KeyboardActions, onValueChanged: (String) -> Unit, modifier: Modifier = Modifier, summary: String? = null, maxSize: Int = 0, // max_size - 1 (in bytes) onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, visualTransformation: VisualTransformation = VisualTransformation.None, )
MagicNumber:BatteryInfo.kt$100
MagicNumber:BatteryInfo.kt$101
MagicNumber:BatteryInfo.kt$14
@@ -43,10 +44,8 @@
ParameterNaming:EditIPv4Preference.kt$onValueChanged
ParameterNaming:EditPasswordPreference.kt$onValueChanged
ParameterNaming:EditTextPreference.kt$onValueChanged
- ParameterNaming:PreferenceFooter.kt$onCancelClicked
ParameterNaming:PreferenceFooter.kt$onNegativeClicked
ParameterNaming:PreferenceFooter.kt$onPositiveClicked
- ParameterNaming:PreferenceFooter.kt$onSaveClicked
ParameterNaming:SlidingSelector.kt$onOptionSelected
PreviewPublic:BatteryInfo.kt$BatteryInfoPreview
PreviewPublic:BatteryInfo.kt$BatteryInfoPreviewSimple
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 16a292a3f..954ef818d 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,8 +26,10 @@ 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
@@ -38,10 +40,12 @@ 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
@@ -207,7 +211,7 @@ fun EditTextPreference(
) {
var isFocused by remember { mutableStateOf(false) }
- Column(modifier = modifier.padding(vertical = 8.dp)) {
+ Column(modifier = modifier) {
OutlinedTextField(
value = value,
singleLine = true,
@@ -227,28 +231,39 @@ fun EditTextPreference(
onValueChanged(it)
}
},
- label = { Text(title) },
+ prefix = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
- trailingIcon = {
- if (trailingIcon != null) {
- trailingIcon()
- } else if (isError) {
+ trailingIcon =
+ if (trailingIcon != null) {
+ { trailingIcon() }
+ } else if (isError) {
+ {
Icon(
imageVector = Icons.TwoTone.Info,
contentDescription = stringResource(id = R.string.error),
tint = MaterialTheme.colorScheme.error,
)
}
+ } 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(
text = summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(top = 4.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 8.dp),
)
}
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt
index df0a1bcfe..a72f4ec38 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/component/PreferenceFooter.kt
@@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -30,25 +32,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import org.meshtastic.core.strings.R
-
-@Composable
-fun PreferenceFooter(
- enabled: Boolean,
- onCancelClicked: () -> Unit,
- onSaveClicked: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- PreferenceFooter(
- enabled = enabled,
- negativeText = R.string.clear_changes,
- onNegativeClicked = onCancelClicked,
- positiveText = R.string.send,
- onPositiveClicked = onSaveClicked,
- modifier = modifier,
- )
-}
+@Deprecated(message = "Use overload that accepts Strings for button text.")
@Composable
fun PreferenceFooter(
enabled: Boolean,
@@ -57,17 +42,36 @@ fun PreferenceFooter(
@StringRes positiveText: Int,
onPositiveClicked: () -> Unit,
modifier: Modifier = Modifier,
+) {
+ PreferenceFooter(
+ enabled = enabled,
+ negativeText = stringResource(id = negativeText),
+ onNegativeClicked = onNegativeClicked,
+ positiveText = stringResource(id = positiveText),
+ onPositiveClicked = onPositiveClicked,
+ modifier = modifier,
+ )
+}
+
+@Composable
+fun PreferenceFooter(
+ enabled: Boolean,
+ negativeText: String,
+ onNegativeClicked: () -> Unit,
+ positiveText: String,
+ onPositiveClicked: () -> Unit,
+ modifier: Modifier = Modifier,
) {
Row(
- modifier = modifier.fillMaxWidth().height(64.dp),
+ modifier = modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(modifier = Modifier.height(48.dp).weight(1f), onClick = onNegativeClicked) {
- Text(text = stringResource(id = negativeText))
+ Text(text = negativeText)
}
- OutlinedButton(modifier = Modifier.height(48.dp).weight(1f), enabled = enabled, onClick = onPositiveClicked) {
- Text(text = stringResource(id = positiveText))
+ Button(modifier = Modifier.height(48.dp).weight(1f), enabled = enabled, onClick = onPositiveClicked) {
+ Text(text = positiveText)
}
}
}
@@ -75,5 +79,11 @@ fun PreferenceFooter(
@Preview(showBackground = true)
@Composable
private fun PreferenceFooterPreview() {
- PreferenceFooter(enabled = true, onCancelClicked = {}, onSaveClicked = {})
+ PreferenceFooter(
+ enabled = true,
+ negativeText = "Cancel",
+ onNegativeClicked = {},
+ positiveText = "Save",
+ onPositiveClicked = {},
+ )
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3692e4754..01de6b43d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -41,6 +41,7 @@ protobuf = "4.32.1"
activity = { module = "androidx.activity:activity" }
activity-compose = { module = "androidx.activity:activity-compose" }
actvity-ktx = { module = "androidx.activity:activity-ktx" }
+annotation = { module = "androidx.annotation:annotation", version = "1.9.1" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "appcompat" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" }