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" }