From 1c3784235e7e20ed88e054097d2e7addbff92dc3 Mon Sep 17 00:00:00 2001 From: Mac DeCourcy <49794076+mdecourcy@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:45:09 -0800 Subject: [PATCH] fix: Multiple bugs - settings text fields, dropdowns, missing override duty cycle, and MQTT icon display (#3833) --- .../geeksville/mesh/service/MeshService.kt | 1 + .../meshtastic/core/database/entity/Packet.kt | 2 +- .../org/meshtastic/core/model/DataPacket.kt | 1 + .../core/ui/component/EditTextPreference.kt | 3 +- .../component/CannedMessageConfigItemList.kt | 2 + .../ExternalNotificationConfigItemList.kt | 6 +- .../radio/component/LoRaConfigItemList.kt | 9 +++ .../radio/component/PositionConfigItemList.kt | 16 ++-- .../radio/component/RadioConfigScreenList.kt | 73 ++++++++----------- 9 files changed, 58 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 12946a2d8..4a23cdf1f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -721,6 +721,7 @@ class MeshService : Service() { rssi = packet.rxRssi, replyId = data.replyId, relayNode = packet.relayNode, + viaMqtt = packet.viaMqtt, ) } diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index 27801bf63..e5f6866c2 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -52,7 +52,7 @@ data class PacketEntity( packetId = packetId, emojis = reactions.toReaction(getNode), replyId = data.replyId, - viaMqtt = node.viaMqtt, + viaMqtt = data.viaMqtt, relayNode = data.relayNode, relays = data.relays, ) diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt index a9b0743b1..c1d9577d3 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt @@ -63,6 +63,7 @@ data class DataPacket( var replyId: Int? = null, // If this is a reply to a previous message, this is the ID of that message var relayNode: Int? = null, var relays: Int = 0, + var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path ) : Parcelable { /** If there was an error with this message, this string describes what was wrong. */ 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 6f90570bd..3e366d062 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 @@ -163,6 +163,7 @@ fun EditTextPreference( onValueChanged: (Double) -> Unit, modifier: Modifier = Modifier, summary: String? = null, + onFocusChanged: (FocusState) -> Unit = {}, ) { var valueState by remember(value) { mutableStateOf(value.toString()) } val decimalSeparators = setOf('.', ',', '٫', '、', '·') // set of possible decimal separators @@ -185,7 +186,7 @@ fun EditTextPreference( } } }, - onFocusChanged = {}, + onFocusChanged = onFocusChanged, modifier = modifier, ) } 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 922477f08..6c38888f1 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 @@ -72,6 +72,8 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), enabled = state.connected, responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, + additionalDirtyCheck = { messagesInput != messages }, + onDiscard = { messagesInput = messages }, onSave = { if (messagesInput != messages) { viewModel.setCannedMessages(messagesInput) 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 f4b83da39..e02fa7b4c 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 @@ -130,6 +130,8 @@ fun ExternalNotificationConfigScreen( enabled = state.connected, responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, + additionalDirtyCheck = { ringtoneInput != ringtone }, + onDiscard = { ringtoneInput = ringtone }, onSave = { if (ringtoneInput != ringtone) { viewModel.setRingtone(ringtoneInput) @@ -259,7 +261,7 @@ fun ExternalNotificationConfigScreen( DropDownPreference( title = stringResource(Res.string.output_duration_milliseconds), items = outputItems.map { it.value to it.toDisplayString() }, - selectedItem = formState.value.outputMs, + selectedItem = formState.value.outputMs.toLong(), enabled = state.connected, onItemSelected = { formState.value = formState.value.copy { outputMs = it.toInt() } }, ) @@ -268,7 +270,7 @@ fun ExternalNotificationConfigScreen( DropDownPreference( title = stringResource(Res.string.nag_timeout_seconds), items = nagItems.map { it.value to it.toDisplayString() }, - selectedItem = formState.value.nagTimeout, + selectedItem = formState.value.nagTimeout.toLong(), enabled = state.connected, onItemSelected = { formState.value = formState.value.copy { nagTimeout = it.toInt() } }, ) 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 d8702e254..a83dba382 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 @@ -47,6 +47,7 @@ import org.meshtastic.core.strings.lora import org.meshtastic.core.strings.modem_preset import org.meshtastic.core.strings.ok_to_mqtt import org.meshtastic.core.strings.options +import org.meshtastic.core.strings.override_duty_cycle import org.meshtastic.core.strings.override_frequency_mhz import org.meshtastic.core.strings.pa_fan_disabled import org.meshtastic.core.strings.region_frequency_plan @@ -169,6 +170,14 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() + SwitchPreference( + title = stringResource(Res.string.override_duty_cycle), + checked = formState.value.overrideDutyCycle, + enabled = state.connected, + onCheckedChange = { formState.value = formState.value.copy { overrideDutyCycle = it } }, + containerColor = CardDefaults.cardColors().containerColor, + ) + HorizontalDivider() val hopLimitItems = remember { hopLimits } DropDownPreference( title = stringResource(Res.string.hop_limit), 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 2a7c6e2a0..1c43ba1a0 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 @@ -149,6 +149,8 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa enabled = state.connected, responseState = state.responseState, onDismissPacketResponse = viewModel::clearPacketResponse, + additionalDirtyCheck = { locationInput != currentPosition }, + onDiscard = { locationInput = currentPosition }, onSave = { if (formState.value.fixedPosition) { if (locationInput != currentPosition) { @@ -233,9 +235,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa value = locationInput.latitude, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { value -> - if (value >= -90 && value <= 90.0) { - locationInput = locationInput.copy(latitude = value) + onValueChanged = { lat: Double -> + if (lat >= -90 && lat <= 90.0) { + locationInput = locationInput.copy(latitude = lat) } }, ) @@ -245,9 +247,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa value = locationInput.longitude, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { value -> - if (value >= -180 && value <= 180.0) { - locationInput = locationInput.copy(longitude = value) + onValueChanged = { lon: Double -> + if (lon >= -180 && lon <= 180.0) { + locationInput = locationInput.copy(longitude = lon) } }, ) @@ -257,7 +259,7 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa value = locationInput.altitude, enabled = state.connected, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) }, + onValueChanged = { alt: Int -> locationInput = locationInput.copy(altitude = alt) }, ) HorizontalDivider() TextButton( diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt index 128761f34..1ac9ce516 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/RadioConfigScreenList.kt @@ -22,20 +22,14 @@ import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp @@ -59,6 +53,8 @@ fun RadioConfigScreenList( enabled: Boolean, onSave: (T) -> Unit, modifier: Modifier = Modifier, + additionalDirtyCheck: () -> Boolean = { false }, + onDiscard: () -> Unit = {}, content: LazyListScope.() -> Unit, ) { val focusManager = LocalFocusManager.current @@ -81,48 +77,37 @@ fun RadioConfigScreenList( ) }, ) { innerPadding -> - val showFooterButtons = configState.isDirty + val showFooterButtons = configState.isDirty || additionalDirtyCheck() - Box(modifier = Modifier.padding(innerPadding)) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - content() + LazyColumn( + modifier = Modifier.padding(innerPadding).fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + content() - item { - AnimatedVisibility( - visible = showFooterButtons, - modifier = Modifier.align(Alignment.BottomCenter), - enter = expandIn(), - exit = shrinkOut(), - ) { - Spacer(modifier = Modifier.height(64.dp)) - } + item { + AnimatedVisibility( + visible = showFooterButtons, + enter = fadeIn() + expandIn(), + exit = fadeOut() + shrinkOut(), + ) { + PreferenceFooter( + enabled = enabled && showFooterButtons, + negativeText = stringResource(Res.string.discard_changes), + onNegativeClicked = { + focusManager.clearFocus() + configState.reset() + onDiscard() + }, + positiveText = stringResource(Res.string.save_changes), + onPositiveClicked = { + focusManager.clearFocus() + onSave(configState.value) + }, + ) } } - - AnimatedVisibility( - visible = showFooterButtons, - modifier = Modifier.align(Alignment.BottomCenter), - enter = fadeIn() + slideInVertically(initialOffsetY = { it }), - exit = fadeOut() + slideOutVertically(targetOffsetY = { it }), - ) { - PreferenceFooter( - enabled = enabled && configState.isDirty, - negativeText = stringResource(Res.string.discard_changes), - onNegativeClicked = { - focusManager.clearFocus() - configState.reset() - }, - positiveText = stringResource(Res.string.save_changes), - onPositiveClicked = { - focusManager.clearFocus() - onSave(configState.value) - }, - ) - } } } }