refactor(settings)!: standardize radio config screens (#3167)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-09-22 21:59:33 -05:00 committed by GitHub
parent d2db37e0d4
commit ddb19b959f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1480 additions and 2651 deletions

View file

@ -47,6 +47,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraphBuilder
@ -153,7 +154,7 @@ fun NavDestination.isConfigRoute(): Boolean =
private inline fun <reified R : Route> NavGraphBuilder.addRadioConfigScreenComposable(
navController: NavHostController,
routeNameString: String,
crossinline screenContent: @Composable (viewModel: RadioConfigViewModel) -> Unit,
crossinline screenContent: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit,
) {
composable<R>(
deepLinks =
@ -167,7 +168,7 @@ private inline fun <reified R : Route> NavGraphBuilder.addRadioConfigScreenCompo
val parentEntry =
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
val viewModel = hiltViewModel<RadioConfigViewModel>(parentEntry)
screenContent(viewModel)
screenContent(navController, viewModel)
}
}
@ -306,71 +307,71 @@ enum class ConfigRoute(
val route: Route,
val icon: ImageVector?,
val type: Int = 0,
val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit,
val screenComposable: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit,
) {
USER(R.string.user, SettingsRoutes.User, Icons.Default.Person, 0, { vm -> UserConfigScreen(vm) }),
USER(R.string.user, SettingsRoutes.User, Icons.Default.Person, 0, { nc, vm -> UserConfigScreen(nc, vm) }),
CHANNELS(
R.string.channels,
SettingsRoutes.ChannelConfig,
Icons.AutoMirrored.Default.List,
0,
{ vm -> ChannelConfigScreen(vm) },
{ nc, vm -> ChannelConfigScreen(nc, vm) },
),
DEVICE(
R.string.device,
SettingsRoutes.Device,
Icons.Default.Router,
AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG_VALUE,
{ vm -> DeviceConfigScreen(vm) },
{ nc, vm -> DeviceConfigScreen(nc, vm) },
),
POSITION(
R.string.position,
SettingsRoutes.Position,
Icons.Default.LocationOn,
AdminProtos.AdminMessage.ConfigType.POSITION_CONFIG_VALUE,
{ vm -> PositionConfigScreen(vm) },
{ nc, vm -> PositionConfigScreen(nc, vm) },
),
POWER(
R.string.power,
SettingsRoutes.Power,
Icons.Default.Power,
AdminProtos.AdminMessage.ConfigType.POWER_CONFIG_VALUE,
{ vm -> PowerConfigScreen(vm) },
{ nc, vm -> PowerConfigScreen(nc, vm) },
),
NETWORK(
R.string.network,
SettingsRoutes.Network,
Icons.Default.Wifi,
AdminProtos.AdminMessage.ConfigType.NETWORK_CONFIG_VALUE,
{ vm -> NetworkConfigScreen(vm) },
{ nc, vm -> NetworkConfigScreen(nc, vm) },
),
DISPLAY(
R.string.display,
SettingsRoutes.Display,
Icons.Default.DisplaySettings,
AdminProtos.AdminMessage.ConfigType.DISPLAY_CONFIG_VALUE,
{ vm -> DisplayConfigScreen(vm) },
{ nc, vm -> DisplayConfigScreen(nc, vm) },
),
LORA(
R.string.lora,
SettingsRoutes.LoRa,
Icons.Default.CellTower,
AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE,
{ vm -> LoRaConfigScreen(vm) },
{ nc, vm -> LoRaConfigScreen(nc, vm) },
),
BLUETOOTH(
R.string.bluetooth,
SettingsRoutes.Bluetooth,
Icons.Default.Bluetooth,
AdminProtos.AdminMessage.ConfigType.BLUETOOTH_CONFIG_VALUE,
{ vm -> BluetoothConfigScreen(vm) },
{ nc, vm -> BluetoothConfigScreen(nc, vm) },
),
SECURITY(
R.string.security,
SettingsRoutes.Security,
Icons.Default.Security,
AdminProtos.AdminMessage.ConfigType.SECURITY_CONFIG_VALUE,
{ vm -> SecurityConfigScreen(vm) },
{ nc, vm -> SecurityConfigScreen(nc, vm) },
),
;
@ -397,98 +398,98 @@ enum class ModuleRoute(
val route: Route,
val icon: ImageVector?,
val type: Int = 0,
val screenComposable: @Composable (viewModel: RadioConfigViewModel) -> Unit,
val screenComposable: @Composable (navController: NavController, viewModel: RadioConfigViewModel) -> Unit,
) {
MQTT(
R.string.mqtt,
SettingsRoutes.MQTT,
Icons.Default.Cloud,
AdminProtos.AdminMessage.ModuleConfigType.MQTT_CONFIG_VALUE,
{ vm -> MQTTConfigScreen(vm) },
{ nc, vm -> MQTTConfigScreen(nc, vm) },
),
SERIAL(
R.string.serial,
SettingsRoutes.Serial,
Icons.Default.Usb,
AdminProtos.AdminMessage.ModuleConfigType.SERIAL_CONFIG_VALUE,
{ vm -> SerialConfigScreen(vm) },
{ nc, vm -> SerialConfigScreen(nc, vm) },
),
EXT_NOTIFICATION(
R.string.external_notification,
SettingsRoutes.ExtNotification,
Icons.Default.Notifications,
AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE,
{ vm -> ExternalNotificationConfigScreen(vm) },
{ nc, vm -> ExternalNotificationConfigScreen(nc, vm) },
),
STORE_FORWARD(
R.string.store_forward,
SettingsRoutes.StoreForward,
Icons.AutoMirrored.Default.Forward,
AdminProtos.AdminMessage.ModuleConfigType.STOREFORWARD_CONFIG_VALUE,
{ vm -> StoreForwardConfigScreen(vm) },
{ nc, vm -> StoreForwardConfigScreen(nc, vm) },
),
RANGE_TEST(
R.string.range_test,
SettingsRoutes.RangeTest,
Icons.Default.Speed,
AdminProtos.AdminMessage.ModuleConfigType.RANGETEST_CONFIG_VALUE,
{ vm -> RangeTestConfigScreen(vm) },
{ nc, vm -> RangeTestConfigScreen(nc, vm) },
),
TELEMETRY(
R.string.telemetry,
SettingsRoutes.Telemetry,
Icons.Default.DataUsage,
AdminProtos.AdminMessage.ModuleConfigType.TELEMETRY_CONFIG_VALUE,
{ vm -> TelemetryConfigScreen(vm) },
{ nc, vm -> TelemetryConfigScreen(nc, vm) },
),
CANNED_MESSAGE(
R.string.canned_message,
SettingsRoutes.CannedMessage,
Icons.AutoMirrored.Default.Message,
AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE,
{ vm -> CannedMessageConfigScreen(vm) },
{ nc, vm -> CannedMessageConfigScreen(nc, vm) },
),
AUDIO(
R.string.audio,
SettingsRoutes.Audio,
Icons.AutoMirrored.Default.VolumeUp,
AdminProtos.AdminMessage.ModuleConfigType.AUDIO_CONFIG_VALUE,
{ vm -> AudioConfigScreen(vm) },
{ nc, vm -> AudioConfigScreen(nc, vm) },
),
REMOTE_HARDWARE(
R.string.remote_hardware,
SettingsRoutes.RemoteHardware,
Icons.Default.SettingsRemote,
AdminProtos.AdminMessage.ModuleConfigType.REMOTEHARDWARE_CONFIG_VALUE,
{ vm -> RemoteHardwareConfigScreen(vm) },
{ nc, vm -> RemoteHardwareConfigScreen(nc, vm) },
),
NEIGHBOR_INFO(
R.string.neighbor_info,
SettingsRoutes.NeighborInfo,
Icons.Default.People,
AdminProtos.AdminMessage.ModuleConfigType.NEIGHBORINFO_CONFIG_VALUE,
{ vm -> NeighborInfoConfigScreen(vm) },
{ nc, vm -> NeighborInfoConfigScreen(nc, vm) },
),
AMBIENT_LIGHTING(
R.string.ambient_lighting,
SettingsRoutes.AmbientLighting,
Icons.Default.LightMode,
AdminProtos.AdminMessage.ModuleConfigType.AMBIENTLIGHTING_CONFIG_VALUE,
{ vm -> AmbientLightingConfigScreen(vm) },
{ nc, vm -> AmbientLightingConfigScreen(nc, vm) },
),
DETECTION_SENSOR(
R.string.detection_sensor,
SettingsRoutes.DetectionSensor,
Icons.Default.Sensors,
AdminProtos.AdminMessage.ModuleConfigType.DETECTIONSENSOR_CONFIG_VALUE,
{ vm -> DetectionSensorConfigScreen(vm) },
{ nc, vm -> DetectionSensorConfigScreen(nc, vm) },
),
PAXCOUNTER(
R.string.paxcounter,
SettingsRoutes.Paxcounter,
Icons.Default.PermScanWifi,
AdminProtos.AdminMessage.ModuleConfigType.PAXCOUNTER_CONFIG_VALUE,
{ vm -> PaxcounterConfigScreen(vm) },
{ nc, vm -> PaxcounterConfigScreen(nc, vm) },
),
;

View file

@ -133,7 +133,6 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
companion object {
fun NavDestination.isTopLevel(): Boolean = listOf<KClass<out Route>>(
ContactsRoutes.Contacts::class,
NodesRoutes.Nodes::class,
MapRoutes.Map::class,
ConnectionsRoutes.Connections::class,
)
@ -356,10 +355,34 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
NodesRoutes.Nodes::class,
NodesRoutes.NodeDetail::class,
SettingsRoutes.Settings::class,
SettingsRoutes.AmbientLighting::class,
SettingsRoutes.LoRa::class,
SettingsRoutes.Security::class,
SettingsRoutes.Audio::class,
SettingsRoutes.Bluetooth::class,
SettingsRoutes.ChannelConfig::class,
SettingsRoutes.DetectionSensor::class,
SettingsRoutes.Display::class,
SettingsRoutes.Telemetry::class,
SettingsRoutes.Network::class,
SettingsRoutes.Paxcounter::class,
SettingsRoutes.Power::class,
SettingsRoutes.Position::class,
SettingsRoutes.User::class,
SettingsRoutes.StoreForward::class,
SettingsRoutes.MQTT::class,
SettingsRoutes.Serial::class,
SettingsRoutes.ExtNotification::class,
SettingsRoutes.CleanNodeDb::class,
SettingsRoutes.DebugPanel::class,
SettingsRoutes.RangeTest::class,
SettingsRoutes.CannedMessage::class,
SettingsRoutes.RemoteHardware::class,
SettingsRoutes.NeighborInfo::class,
)
.none { this.hasRoute(it) }
AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) {
AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: false) {
MainAppBar(
viewModel = uIViewModel,
navController = navController,

View file

@ -186,9 +186,16 @@ fun SettingsScreen(
topBar = {
MainAppBar(
title = stringResource(R.string.bottom_nav_settings),
subtitle =
if (state.isLocal) {
ourNode?.user?.longName
} else {
val remoteName = viewModel.destNode.value?.user?.longName ?: ""
stringResource(R.string.remotely_administrating, remoteName)
},
ourNode = ourNode,
isConnected = isConnected,
showNodeChip = ourNode != null && isConnected,
showNodeChip = ourNode != null && isConnected && state.isLocal,
canNavigateUp = false,
onNavigateUp = {},
actions = {},

View file

@ -17,67 +17,50 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun AmbientLightingConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val ambientLightingConfig = state.moduleConfig.ambientLighting
val formState = rememberConfigState(initialValue = ambientLightingConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
AmbientLightingConfigItemList(
ambientLightingConfig = state.moduleConfig.ambientLighting,
RadioConfigScreenList(
title = stringResource(id = R.string.ambient_lighting),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { ambientLightingInput ->
val config = moduleConfig { ambientLighting = ambientLightingInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { ambientLighting = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun AmbientLightingConfigItemList(
ambientLightingConfig: ModuleConfigProtos.ModuleConfig.AmbientLightingConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.AmbientLightingConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var ambientLightingInput by rememberSaveable { mutableStateOf(ambientLightingConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.ambient_lighting_config)) }
item {
SwitchPreference(
title = stringResource(R.string.led_state),
checked = ambientLightingInput.ledState,
enabled = enabled,
onCheckedChange = { ambientLightingInput = ambientLightingInput.copy { ledState = it } },
checked = formState.value.ledState,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ledState = it } },
)
}
item { HorizontalDivider() }
@ -85,65 +68,41 @@ fun AmbientLightingConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.current),
value = ambientLightingInput.current,
enabled = enabled,
value = formState.value.current,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { current = it } },
onValueChanged = { formState.value = formState.value.copy { current = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.red),
value = ambientLightingInput.red,
enabled = enabled,
value = formState.value.red,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } },
onValueChanged = { formState.value = formState.value.copy { red = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.green),
value = ambientLightingInput.green,
enabled = enabled,
value = formState.value.green,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } },
onValueChanged = { formState.value = formState.value.copy { green = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.blue),
value = ambientLightingInput.blue,
enabled = enabled,
value = formState.value.blue,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { blue = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && ambientLightingInput != ambientLightingConfig,
onCancelClicked = {
focusManager.clearFocus()
ambientLightingInput = ambientLightingConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ambientLightingInput)
},
onValueChanged = { formState.value = formState.value.copy { blue = it } },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun AmbientLightingConfigPreview() {
AmbientLightingConfigItemList(
ambientLightingConfig = ModuleConfigProtos.ModuleConfig.AmbientLightingConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -17,66 +17,52 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun AudioConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val audioConfig = state.moduleConfig.audio
val formState = rememberConfigState(initialValue = audioConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
AudioConfigItemList(
audioConfig = state.moduleConfig.audio,
RadioConfigScreenList(
title = stringResource(id = R.string.audio),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { audioInput ->
val config = moduleConfig { audio = audioInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { audio = it }
viewModel.setModuleConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun AudioConfigItemList(audioConfig: AudioConfig, enabled: Boolean, onSaveClicked: (AudioConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var audioInput by rememberSaveable { mutableStateOf(audioConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.audio_config)) }
item {
SwitchPreference(
title = stringResource(R.string.codec_2_enabled),
checked = audioInput.codec2Enabled,
enabled = enabled,
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } },
checked = formState.value.codec2Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { codec2Enabled = it } },
)
}
item { HorizontalDivider() }
@ -84,85 +70,65 @@ fun AudioConfigItemList(audioConfig: AudioConfig, enabled: Boolean, onSaveClicke
item {
EditTextPreference(
title = stringResource(R.string.ptt_pin),
value = audioInput.pttPin,
enabled = enabled,
value = formState.value.pttPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { pttPin = it } },
onValueChanged = { formState.value = formState.value.copy { pttPin = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.codec2_sample_rate),
enabled = enabled,
enabled = state.connected,
items =
AudioConfig.Audio_Baud.entries
.filter { it != AudioConfig.Audio_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = audioInput.bitrate,
onItemSelected = { audioInput = audioInput.copy { bitrate = it } },
selectedItem = formState.value.bitrate,
onItemSelected = { formState.value = formState.value.copy { bitrate = it } },
)
}
item { Divider() }
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.i2s_word_select),
value = audioInput.i2SWs,
enabled = enabled,
value = formState.value.i2SWs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SWs = it } },
onValueChanged = { formState.value = formState.value.copy { i2SWs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_data_in),
value = audioInput.i2SSd,
enabled = enabled,
value = formState.value.i2SSd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSd = it } },
onValueChanged = { formState.value = formState.value.copy { i2SSd = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_data_out),
value = audioInput.i2SDin,
enabled = enabled,
value = formState.value.i2SDin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SDin = it } },
onValueChanged = { formState.value = formState.value.copy { i2SDin = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.i2s_clock),
value = audioInput.i2SSck,
enabled = enabled,
value = formState.value.i2SSck,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { audioInput = audioInput.copy { i2SSck = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && audioInput != audioConfig,
onCancelClicked = {
focusManager.clearFocus()
audioInput = audioConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(audioInput)
},
onValueChanged = { formState.value = formState.value.copy { i2SSck = it } },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun AudioConfigPreview() {
AudioConfigItemList(audioConfig = AudioConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,68 +17,52 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun BluetoothConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val bluetoothConfig = state.radioConfig.bluetooth
val formState = rememberConfigState(initialValue = bluetoothConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
BluetoothConfigItemList(
bluetoothConfig = state.radioConfig.bluetooth,
RadioConfigScreenList(
title = stringResource(id = R.string.bluetooth),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { bluetoothInput ->
val config = config { bluetooth = bluetoothInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { bluetooth = it }
viewModel.setConfig(config)
},
)
}
@Composable
fun BluetoothConfigItemList(
bluetoothConfig: BluetoothConfig,
enabled: Boolean,
onSaveClicked: (BluetoothConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var bluetoothInput by rememberSaveable { mutableStateOf(bluetoothConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.bluetooth_config)) }
item {
SwitchPreference(
title = stringResource(R.string.bluetooth_enabled),
checked = bluetoothInput.enabled,
enabled = enabled,
onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -86,13 +70,13 @@ fun BluetoothConfigItemList(
item {
DropDownPreference(
title = stringResource(R.string.pairing_mode),
enabled = enabled,
enabled = state.connected,
items =
BluetoothConfig.PairingMode.entries
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = bluetoothInput.mode,
onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
}
item { HorizontalDivider() }
@ -100,35 +84,15 @@ fun BluetoothConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.fixed_pin),
value = bluetoothInput.fixedPin,
enabled = enabled,
value = formState.value.fixedPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it.toString().length == 6) { // ensure 6 digits
bluetoothInput = bluetoothInput.copy { fixedPin = it }
formState.value = formState.value.copy { fixedPin = it }
}
},
)
}
item {
PreferenceFooter(
enabled = enabled && bluetoothInput != bluetoothConfig,
onCancelClicked = {
focusManager.clearFocus()
bluetoothInput = bluetoothConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(bluetoothInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun BluetoothConfigPreview() {
BluetoothConfigItemList(bluetoothConfig = BluetoothConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,8 +17,6 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
@ -27,69 +25,57 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun CannedMessageConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val cannedMessageConfig = state.moduleConfig.cannedMessage
val messages = state.cannedMessageMessages
val formState = rememberConfigState(initialValue = cannedMessageConfig)
var messagesInput by rememberSaveable(messages) { mutableStateOf(messages) }
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
CannedMessageConfigItemList(
messages = state.cannedMessageMessages,
cannedMessageConfig = state.moduleConfig.cannedMessage,
RadioConfigScreenList(
title = stringResource(id = R.string.canned_message),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { messagesInput, cannedMessageInput ->
if (messagesInput != state.cannedMessageMessages) {
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
if (messagesInput != messages) {
viewModel.setCannedMessages(messagesInput)
}
if (cannedMessageInput != state.moduleConfig.cannedMessage) {
val config = moduleConfig { cannedMessage = cannedMessageInput }
if (formState.value != cannedMessageConfig) {
val config = moduleConfig { cannedMessage = formState.value }
viewModel.setModuleConfig(config)
}
},
)
}
@Composable
fun CannedMessageConfigItemList(
messages: String,
cannedMessageConfig: CannedMessageConfig,
enabled: Boolean,
onSaveClicked: (messages: String, config: CannedMessageConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var messagesInput by rememberSaveable { mutableStateOf(messages) }
var cannedMessageInput by rememberSaveable { mutableStateOf(cannedMessageConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.canned_message_config)) }
item {
SwitchPreference(
title = stringResource(R.string.canned_message_enabled),
checked = cannedMessageInput.enabled,
enabled = enabled,
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -97,9 +83,9 @@ fun CannedMessageConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.rotary_encoder_1_enabled),
checked = cannedMessageInput.rotary1Enabled,
enabled = enabled,
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it } },
checked = formState.value.rotary1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { rotary1Enabled = it } },
)
}
item { HorizontalDivider() }
@ -107,43 +93,43 @@ fun CannedMessageConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_a_port),
value = cannedMessageInput.inputbrokerPinA,
enabled = enabled,
value = formState.value.inputbrokerPinA,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it } },
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinA = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_b_port),
value = cannedMessageInput.inputbrokerPinB,
enabled = enabled,
value = formState.value.inputbrokerPinB,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it } },
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinB = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_for_rotary_encoder_press_port),
value = cannedMessageInput.inputbrokerPinPress,
enabled = enabled,
value = formState.value.inputbrokerPinPress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it } },
onValueChanged = { formState.value = formState.value.copy { inputbrokerPinPress = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_press),
enabled = enabled,
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventPress,
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it } },
selectedItem = formState.value.inputbrokerEventPress,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventPress = it } },
)
}
item { HorizontalDivider() }
@ -151,13 +137,13 @@ fun CannedMessageConfigItemList(
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_cw),
enabled = enabled,
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCw,
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it } },
selectedItem = formState.value.inputbrokerEventCw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCw = it } },
)
}
item { HorizontalDivider() }
@ -165,13 +151,13 @@ fun CannedMessageConfigItemList(
item {
DropDownPreference(
title = stringResource(R.string.generate_input_event_on_ccw),
enabled = enabled,
enabled = state.connected,
items =
CannedMessageConfig.InputEventChar.entries
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
.map { it to it.name },
selectedItem = cannedMessageInput.inputbrokerEventCcw,
onItemSelected = { cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it } },
selectedItem = formState.value.inputbrokerEventCcw,
onItemSelected = { formState.value = formState.value.copy { inputbrokerEventCcw = it } },
)
}
item { HorizontalDivider() }
@ -179,9 +165,9 @@ fun CannedMessageConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.up_down_select_input_enabled),
checked = cannedMessageInput.updown1Enabled,
enabled = enabled,
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it } },
checked = formState.value.updown1Enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { updown1Enabled = it } },
)
}
item { HorizontalDivider() }
@ -189,23 +175,23 @@ fun CannedMessageConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.allow_input_source),
value = cannedMessageInput.allowInputSource,
value = formState.value.allowInputSource,
maxSize = 63, // allow_input_source max_size:16
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { cannedMessageInput = cannedMessageInput.copy { allowInputSource = it } },
onValueChanged = { formState.value = formState.value.copy { allowInputSource = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.send_bell),
checked = cannedMessageInput.sendBell,
enabled = enabled,
onCheckedChange = { cannedMessageInput = cannedMessageInput.copy { sendBell = it } },
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
@ -215,7 +201,7 @@ fun CannedMessageConfigItemList(
title = stringResource(R.string.messages),
value = messagesInput,
maxSize = 200, // messages max_size:201
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
@ -223,31 +209,5 @@ fun CannedMessageConfigItemList(
onValueChanged = { messagesInput = it },
)
}
item {
PreferenceFooter(
enabled = enabled && cannedMessageInput != cannedMessageConfig || messagesInput != messages,
onCancelClicked = {
focusManager.clearFocus()
messagesInput = messages
cannedMessageInput = cannedMessageConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(messagesInput, cannedMessageInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun CannedMessageConfigPreview() {
CannedMessageConfigItemList(
messages = "",
cannedMessageConfig = CannedMessageConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
)
}

View file

@ -47,6 +47,7 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -68,6 +69,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.channelSettings
@ -168,7 +170,7 @@ fun ChannelSelection(
}
@Composable
fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun ChannelConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
@ -176,6 +178,8 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
}
ChannelSettingsItemList(
title = stringResource(id = R.string.channels),
onBack = { navController.popBackStack() },
settingsList = state.channelList,
loraConfig = state.radioConfig.lora,
maxChannels = viewModel.maxChannels,
@ -188,6 +192,8 @@ fun ChannelConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
private fun ChannelSettingsItemList(
title: String,
onBack: () -> Unit,
settingsList: List<ChannelSettings>,
loraConfig: LoRaConfig,
maxChannels: Int = 8,
@ -243,104 +249,116 @@ private fun ChannelSettingsItemList(
ChannelLegendDialog(fwVersion) { showChannelLegendDialog = false }
}
Box(modifier = Modifier.fillMaxSize().clickable(onClick = {}, enabled = false)) {
Column {
ChannelsConfigHeader(
frequency =
if (loraConfig.overrideFrequency != 0f) {
loraConfig.overrideFrequency
} else {
primaryChannel.radioFreq
},
slot =
if (loraConfig.channelNum != 0) {
loraConfig.channelNum
} else {
primaryChannel.channelNum
},
)
Text(
text = stringResource(R.string.press_and_drag),
fontSize = 11.sp,
modifier = Modifier.padding(start = 16.dp),
)
ChannelLegend { showChannelLegendDialog = true }
val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList())
LazyColumn(
modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp),
) {
dragDropItemsIndexed(items = settingsListInput, dragDropState = dragDropState) {
index,
channel,
isDragging,
->
ChannelCard(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
channelSettings = channel,
loraConfig = loraConfig,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { settingsListInput.removeAt(index) },
sharesLocation = locationChannel == index,
)
}
item {
PreferenceFooter(
enabled = enabled && isEditing,
negativeText = R.string.cancel,
onNegativeClicked = {
focusManager.clearFocus()
settingsListInput.clear()
settingsListInput.addAll(settingsList)
},
positiveText = R.string.send,
onPositiveClicked = {
focusManager.clearFocus()
onPositiveClicked(settingsListInput)
},
)
Scaffold(
floatingActionButton = {
if (maxChannels > settingsListInput.size) {
FloatingActionButton(
onClick = {
if (maxChannels > settingsListInput.size) {
settingsListInput.add(channelSettings { psk = Channel.default.settings.psk })
showEditChannelDialog = settingsListInput.lastIndex
}
},
modifier = Modifier.padding(16.dp),
) {
Icon(Icons.TwoTone.Add, stringResource(R.string.add))
}
}
}
},
) { innerPadding ->
Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
Column {
ChannelsConfigHeader(
frequency =
if (loraConfig.overrideFrequency != 0f) {
loraConfig.overrideFrequency
} else {
primaryChannel.radioFreq
},
slot =
if (loraConfig.channelNum != 0) {
loraConfig.channelNum
} else {
primaryChannel.channelNum
},
)
Text(
text = stringResource(R.string.press_and_drag),
fontSize = 11.sp,
modifier = Modifier.padding(start = 16.dp),
)
AnimatedVisibility(
visible = maxChannels > settingsListInput.size,
modifier = Modifier.align(Alignment.BottomEnd),
enter =
slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
),
exit =
slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
),
) {
FloatingActionButton(
onClick = {
if (maxChannels > settingsListInput.size) {
settingsListInput.add(channelSettings { psk = Channel.default.settings.psk })
showEditChannelDialog = settingsListInput.lastIndex
ChannelLegend { showChannelLegendDialog = true }
val locationChannel = determineLocationSharingChannel(fwVersion, settingsListInput.toList())
LazyColumn(
modifier =
Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp),
) {
dragDropItemsIndexed(items = settingsListInput, dragDropState = dragDropState) {
index,
channel,
isDragging,
->
ChannelCard(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
channelSettings = channel,
loraConfig = loraConfig,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { settingsListInput.removeAt(index) },
sharesLocation = locationChannel == index,
)
}
},
modifier = Modifier.padding(16.dp),
) {
Icon(Icons.TwoTone.Add, stringResource(R.string.add))
item { Spacer(modifier = Modifier.weight(1f)) }
item {
PreferenceFooter(
enabled = enabled && isEditing,
negativeText = R.string.cancel,
onNegativeClicked = {
focusManager.clearFocus()
settingsListInput.clear()
settingsListInput.addAll(settingsList)
},
positiveText = R.string.send,
onPositiveClicked = {
focusManager.clearFocus()
onPositiveClicked(settingsListInput)
},
)
}
}
}
AnimatedVisibility(
visible = maxChannels > settingsListInput.size,
modifier = Modifier.align(Alignment.BottomEnd),
enter =
slideInHorizontally(
initialOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
),
exit =
slideOutHorizontally(
targetOffsetX = { it },
animationSpec = tween(durationMillis = 600, easing = FastOutSlowInEasing),
),
) {}
}
}
}
@Composable
private fun ChannelsConfigHeader(frequency: Float, slot: Int) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
PreferenceCategory(text = stringResource(R.string.channels))
Column {
Text(text = "${stringResource(R.string.freq)}: ${frequency}MHz", fontSize = 11.sp)
@ -380,6 +398,8 @@ private fun determineLocationSharingChannel(firmwareVersion: DeviceVersion, sett
@Composable
private fun ChannelSettingsPreview() {
ChannelSettingsItemList(
title = "Channels",
onBack = {},
settingsList =
listOf(
channelSettings {

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.google.protobuf.MessageLite
/**
* A state holder for managing config data within a Composable.
*
* This class encapsulates the common logic for handling editable state that is derived from an initial value. It tracks
* whether the current value has been modified ("dirty"), and provides simple methods to save the changes or reset to
* the initial state.
*
* @param T The type of the data being managed, typically a Protobuf message.
* @property initialValue The original, unmodified value of the config data.
*/
class ConfigState<T : MessageLite>(private val initialValue: T) {
var value by mutableStateOf(initialValue)
val isDirty: Boolean
get() = value != initialValue
fun reset() {
value = initialValue
}
companion object {
fun <T : MessageLite> saver(initialValue: T): Saver<ConfigState<T>, ByteArray> = Saver(
save = { it.value.toByteArray() },
restore = {
ConfigState(initialValue).apply {
@Suppress("UNCHECKED_CAST")
value = initialValue.parserForType.parseFrom(it) as T
}
},
)
}
}
/**
* Creates and remembers a [ConfigState] instance, correctly handling process death and recomposition. When the
* `initialValue` changes, the config state will be reset.
*
* @param initialValue The initial value to populate the config with. The config will be reset if this value changes
* across recompositions.
*/
@Composable
fun <T : MessageLite> rememberConfigState(initialValue: T): ConfigState<T> =
rememberSaveable(initialValue, saver = ConfigState.saver(initialValue)) { ConfigState(initialValue) }

View file

@ -17,72 +17,55 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun DetectionSensorConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val detectionSensorConfig = state.moduleConfig.detectionSensor
val formState = rememberConfigState(initialValue = detectionSensorConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
DetectionSensorConfigItemList(
detectionSensorConfig = state.moduleConfig.detectionSensor,
RadioConfigScreenList(
title = stringResource(id = R.string.detection_sensor),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { detectionSensorInput ->
val config = moduleConfig { detectionSensor = detectionSensorInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { detectionSensor = it }
viewModel.setModuleConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun DetectionSensorConfigItemList(
detectionSensorConfig: ModuleConfig.DetectionSensorConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfig.DetectionSensorConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var detectionSensorInput by rememberSaveable { mutableStateOf(detectionSensorConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.detection_sensor_config)) }
item {
SwitchPreference(
title = stringResource(R.string.detection_sensor_enabled),
checked = detectionSensorInput.enabled,
enabled = enabled,
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -90,29 +73,29 @@ fun DetectionSensorConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.minimum_broadcast_seconds),
value = detectionSensorInput.minimumBroadcastSecs,
enabled = enabled,
value = formState.value.minimumBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it } },
onValueChanged = { formState.value = formState.value.copy { minimumBroadcastSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.state_broadcast_seconds),
value = detectionSensorInput.stateBroadcastSecs,
enabled = enabled,
value = formState.value.stateBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it } },
onValueChanged = { formState.value = formState.value.copy { stateBroadcastSecs = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.send_bell_with_alert_message),
checked = detectionSensorInput.sendBell,
enabled = enabled,
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { sendBell = it } },
checked = formState.value.sendBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sendBell = it } },
)
}
item { HorizontalDivider() }
@ -120,37 +103,37 @@ fun DetectionSensorConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.friendly_name),
value = detectionSensorInput.name,
value = formState.value.name,
maxSize = 19, // name max_size:20
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { name = it } },
onValueChanged = { formState.value = formState.value.copy { name = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gpio_pin_to_monitor),
value = detectionSensorInput.monitorPin,
enabled = enabled,
value = formState.value.monitorPin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { detectionSensorInput = detectionSensorInput.copy { monitorPin = it } },
onValueChanged = { formState.value = formState.value.copy { monitorPin = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.detection_trigger_type),
enabled = enabled,
enabled = state.connected,
items =
ModuleConfig.DetectionSensorConfig.TriggerType.entries
.filter { it != ModuleConfig.DetectionSensorConfig.TriggerType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = detectionSensorInput.detectionTriggerType,
onItemSelected = { detectionSensorInput = detectionSensorInput.copy { detectionTriggerType = it } },
selectedItem = formState.value.detectionTriggerType,
onItemSelected = { formState.value = formState.value.copy { detectionTriggerType = it } },
)
}
item { HorizontalDivider() }
@ -158,35 +141,11 @@ fun DetectionSensorConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.use_input_pullup_mode),
checked = detectionSensorInput.usePullup,
enabled = enabled,
onCheckedChange = { detectionSensorInput = detectionSensorInput.copy { usePullup = it } },
checked = formState.value.usePullup,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePullup = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && detectionSensorInput != detectionSensorConfig,
onCancelClicked = {
focusManager.clearFocus()
detectionSensorInput = detectionSensorConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(detectionSensorInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DetectionSensorConfigPreview() {
DetectionSensorConfigItemList(
detectionSensorConfig = ModuleConfig.DetectionSensorConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -20,9 +20,7 @@ package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
@ -46,16 +44,15 @@ import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@ -91,24 +88,135 @@ private val DeviceConfig.RebroadcastMode.description: Int
}
@Composable
fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun DeviceConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
val deviceConfig = state.radioConfig.device
val formState = rememberConfigState(initialValue = deviceConfig)
var selectedRole by rememberSaveable { mutableStateOf(formState.value.role) }
val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER)
if (selectedRole != formState.value.role) {
if (selectedRole in infrastructureRoles) {
RouterRoleConfirmationDialog(
onDismiss = { selectedRole = formState.value.role },
onConfirm = { formState.value = formState.value.copy { role = selectedRole } },
)
} else {
formState.value = formState.value.copy { role = selectedRole }
}
}
DeviceConfigItemList(
deviceConfig = state.radioConfig.device,
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.device),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { deviceInput ->
val config = config { device = deviceInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { device = it }
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() }
item { PreferenceCategory(text = stringResource(R.string.debug)) }
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 } },
)
}
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 } },
)
}
}
}
@Suppress("LongMethod")
@Composable
fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
val dialogTitle = stringResource(R.string.are_you_sure)
@ -141,140 +249,3 @@ fun RouterRoleConfirmationDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } },
)
}
@Suppress("LongMethod")
@Composable
fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveClicked: (DeviceConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var deviceInput by rememberSaveable { mutableStateOf(deviceConfig) }
var selectedRole by rememberSaveable { mutableStateOf(deviceInput.role) }
val infrastructureRoles = listOf(DeviceConfig.Role.ROUTER, DeviceConfig.Role.REPEATER)
if (selectedRole != deviceInput.role) {
if (selectedRole in infrastructureRoles) {
RouterRoleConfirmationDialog(
onDismiss = { selectedRole = deviceInput.role },
onConfirm = { deviceInput = deviceInput.copy { role = selectedRole } },
)
} else {
deviceInput = deviceInput.copy { role = selectedRole }
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
DropDownPreference(
title = stringResource(R.string.role),
enabled = enabled,
selectedItem = deviceInput.role,
onItemSelected = { selectedRole = it },
summary = stringResource(id = deviceInput.role.description),
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.rebroadcast_mode),
enabled = enabled,
selectedItem = deviceInput.rebroadcastMode,
onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } },
summary = stringResource(id = deviceInput.rebroadcastMode.description),
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.nodeinfo_broadcast_interval),
value = deviceInput.nodeInfoBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.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 = deviceInput.doubleTapAsButtonPress,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.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 = !deviceInput.disableTripleClick,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = !it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.led_heartbeat),
summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary),
checked = !deviceInput.ledHeartbeatDisabled,
enabled = enabled,
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = !it } },
)
}
item { HorizontalDivider() }
item { PreferenceCategory(text = stringResource(R.string.debug)) }
item {
EditTextPreference(
title = stringResource(R.string.time_zone),
value = deviceInput.tzdef,
summary = stringResource(id = R.string.config_device_tzdef_summary),
maxSize = 64, // tzdef max_size:65
enabled = enabled,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { tzdef = it } },
)
}
item { PreferenceCategory(text = stringResource(R.string.gpio)) }
item {
EditTextPreference(
title = stringResource(R.string.button_gpio),
value = deviceInput.buttonGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.buzzer_gpio),
value = deviceInput.buzzerGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && deviceInput != deviceConfig,
onCancelClicked = {
focusManager.clearFocus()
deviceInput = deviceConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(deviceInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DeviceConfigPreview() {
DeviceConfigItemList(deviceConfig = DeviceConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,65 +17,52 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun DisplayConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val displayConfig = state.radioConfig.display
val formState = rememberConfigState(initialValue = displayConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
DisplayConfigItemList(
displayConfig = state.radioConfig.display,
RadioConfigScreenList(
title = stringResource(id = R.string.display),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { displayInput ->
val config = config { display = displayInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { display = it }
viewModel.setConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSaveClicked: (DisplayConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var displayInput by rememberSaveable { mutableStateOf(displayConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
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 = displayInput.compassNorthTop,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } },
checked = formState.value.compassNorthTop,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { compassNorthTop = it } },
)
}
item { HorizontalDivider() }
@ -83,9 +70,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
SwitchPreference(
title = stringResource(R.string.use_12h_format),
summary = stringResource(R.string.display_time_in_12h_format),
enabled = enabled,
checked = displayInput.use12HClock,
onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } },
enabled = state.connected,
checked = formState.value.use12HClock,
onCheckedChange = { formState.value = formState.value.copy { use12HClock = it } },
)
}
item { HorizontalDivider() }
@ -93,9 +80,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
SwitchPreference(
title = stringResource(R.string.bold_heading),
summary = stringResource(id = R.string.config_display_heading_bold_summary),
checked = displayInput.headingBold,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { headingBold = it } },
checked = formState.value.headingBold,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { headingBold = it } },
)
}
item { HorizontalDivider() }
@ -103,13 +90,13 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
DropDownPreference(
title = stringResource(R.string.display_units),
summary = stringResource(id = R.string.config_display_units_summary),
enabled = enabled,
enabled = state.connected,
items =
DisplayConfig.DisplayUnits.entries
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.units,
onItemSelected = { displayInput = displayInput.copy { units = it } },
selectedItem = formState.value.units,
onItemSelected = { formState.value = formState.value.copy { units = it } },
)
}
item { HorizontalDivider() }
@ -119,10 +106,10 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
EditTextPreference(
title = stringResource(R.string.screen_on_for),
summary = stringResource(id = R.string.config_display_screen_on_secs_summary),
value = displayInput.screenOnSecs,
enabled = enabled,
value = formState.value.screenOnSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } },
onValueChanged = { formState.value = formState.value.copy { screenOnSecs = it } },
)
}
item { HorizontalDivider() }
@ -131,10 +118,10 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
EditTextPreference(
title = stringResource(R.string.carousel_interval),
summary = stringResource(id = R.string.config_display_auto_screen_carousel_secs_summary),
value = displayInput.autoScreenCarouselSecs,
enabled = enabled,
value = formState.value.autoScreenCarouselSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } },
onValueChanged = { formState.value = formState.value.copy { autoScreenCarouselSecs = it } },
)
}
item { HorizontalDivider() }
@ -142,9 +129,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
SwitchPreference(
title = stringResource(R.string.wake_on_tap_or_motion),
summary = stringResource(id = R.string.config_display_wake_on_tap_or_motion_summary),
checked = displayInput.wakeOnTapOrMotion,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } },
checked = formState.value.wakeOnTapOrMotion,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wakeOnTapOrMotion = it } },
)
}
item { HorizontalDivider() }
@ -152,9 +139,9 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
SwitchPreference(
title = stringResource(R.string.flip_screen),
summary = stringResource(id = R.string.config_display_flip_screen_summary),
checked = displayInput.flipScreen,
enabled = enabled,
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } },
checked = formState.value.flipScreen,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { flipScreen = it } },
)
}
item { HorizontalDivider() }
@ -162,13 +149,13 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
DropDownPreference(
title = stringResource(R.string.display_mode),
summary = stringResource(id = R.string.config_display_displaymode_summary),
enabled = enabled,
enabled = state.connected,
items =
DisplayConfig.DisplayMode.entries
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.displaymode,
onItemSelected = { displayInput = displayInput.copy { displaymode = it } },
selectedItem = formState.value.displaymode,
onItemSelected = { formState.value = formState.value.copy { displaymode = it } },
)
}
item { HorizontalDivider() }
@ -176,48 +163,28 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave
DropDownPreference(
title = stringResource(R.string.oled_type),
summary = stringResource(id = R.string.config_display_oled_summary),
enabled = enabled,
enabled = state.connected,
items =
DisplayConfig.OledType.entries
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.oled,
onItemSelected = { displayInput = displayInput.copy { oled = it } },
selectedItem = formState.value.oled,
onItemSelected = { formState.value = formState.value.copy { oled = it } },
)
}
item { HorizontalDivider() }
item {
DropDownPreference(
title = stringResource(R.string.compass_orientation),
enabled = enabled,
enabled = state.connected,
items =
DisplayConfig.CompassOrientation.entries
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
.map { it to it.name },
selectedItem = displayInput.compassOrientation,
onItemSelected = { displayInput = displayInput.copy { compassOrientation = it } },
selectedItem = formState.value.compassOrientation,
onItemSelected = { formState.value = formState.value.copy { compassOrientation = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && displayInput != displayConfig,
onCancelClicked = {
focusManager.clearFocus()
displayInput = displayConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(displayInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DisplayConfigPreview() {
DisplayConfigItemList(displayConfig = DisplayConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,8 +17,6 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
@ -27,80 +25,69 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.common.components.TextDividerPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun ExternalNotificationConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun ExternalNotificationConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val extNotificationConfig = state.moduleConfig.externalNotification
val ringtone = state.ringtone
val formState = rememberConfigState(initialValue = extNotificationConfig)
var ringtoneInput by rememberSaveable(ringtone) { mutableStateOf(ringtone) }
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
ExternalNotificationConfigItemList(
ringtone = state.ringtone,
extNotificationConfig = state.moduleConfig.externalNotification,
RadioConfigScreenList(
title = stringResource(id = R.string.external_notification),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { ringtoneInput, extNotificationInput ->
if (ringtoneInput != state.ringtone) {
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
if (ringtoneInput != ringtone) {
viewModel.setRingtone(ringtoneInput)
}
if (extNotificationInput != state.moduleConfig.externalNotification) {
val config = moduleConfig { externalNotification = extNotificationInput }
if (formState.value != extNotificationConfig) {
val config = moduleConfig { externalNotification = formState.value }
viewModel.setModuleConfig(config)
}
},
)
}
@Composable
fun ExternalNotificationConfigItemList(
ringtone: String,
extNotificationConfig: ExternalNotificationConfig,
enabled: Boolean,
onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var ringtoneInput by rememberSaveable { mutableStateOf(ringtone) }
var externalNotificationInput by rememberSaveable { mutableStateOf(extNotificationConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.external_notification_config)) }
item {
SwitchPreference(
title = stringResource(R.string.external_notification_enabled),
checked = externalNotificationInput.enabled,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { this.enabled = it } },
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 = enabled) }
item {
TextDividerPreference(stringResource(R.string.notifications_on_message_receipt), enabled = state.connected)
}
item {
SwitchPreference(
title = stringResource(R.string.alert_message_led),
checked = externalNotificationInput.alertMessage,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertMessage = it } },
checked = formState.value.alertMessage,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessage = it } },
)
}
item { HorizontalDivider() }
@ -108,11 +95,9 @@ fun ExternalNotificationConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.alert_message_buzzer),
checked = externalNotificationInput.alertMessageBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertMessageBuzzer = it }
},
checked = formState.value.alertMessageBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertMessageBuzzer = it } },
)
}
item { HorizontalDivider() }
@ -120,22 +105,25 @@ fun ExternalNotificationConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.alert_message_vibra),
checked = externalNotificationInput.alertMessageVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertMessageVibra = it }
},
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 = enabled) }
item {
TextDividerPreference(
stringResource(R.string.notifications_on_alert_bell_receipt),
enabled = state.connected,
)
}
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_led),
checked = externalNotificationInput.alertBell,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { alertBell = it } },
checked = formState.value.alertBell,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBell = it } },
)
}
item { HorizontalDivider() }
@ -143,11 +131,9 @@ fun ExternalNotificationConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_buzzer),
checked = externalNotificationInput.alertBellBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertBellBuzzer = it }
},
checked = formState.value.alertBellBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellBuzzer = it } },
)
}
item { HorizontalDivider() }
@ -155,11 +141,9 @@ fun ExternalNotificationConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.alert_bell_vibra),
checked = externalNotificationInput.alertBellVibra,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { alertBellVibra = it }
},
checked = formState.value.alertBellVibra,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { alertBellVibra = it } },
)
}
item { HorizontalDivider() }
@ -167,20 +151,20 @@ fun ExternalNotificationConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.output_led_gpio),
value = externalNotificationInput.output,
enabled = enabled,
value = formState.value.output,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { output = it } },
onValueChanged = { formState.value = formState.value.copy { output = it } },
)
}
if (externalNotificationInput.output != 0) {
if (formState.value.output != 0) {
item {
SwitchPreference(
title = stringResource(R.string.output_led_active_high),
checked = externalNotificationInput.active,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { active = it } },
checked = formState.value.active,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { active = it } },
)
}
}
@ -189,20 +173,20 @@ fun ExternalNotificationConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.output_buzzer_gpio),
value = externalNotificationInput.outputBuzzer,
enabled = enabled,
value = formState.value.outputBuzzer,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it } },
onValueChanged = { formState.value = formState.value.copy { outputBuzzer = it } },
)
}
if (externalNotificationInput.outputBuzzer != 0) {
if (formState.value.outputBuzzer != 0) {
item {
SwitchPreference(
title = stringResource(R.string.use_pwm_buzzer),
checked = externalNotificationInput.usePwm,
enabled = enabled,
onCheckedChange = { externalNotificationInput = externalNotificationInput.copy { usePwm = it } },
checked = formState.value.usePwm,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePwm = it } },
)
}
}
@ -211,30 +195,30 @@ fun ExternalNotificationConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.output_vibra_gpio),
value = externalNotificationInput.outputVibra,
enabled = enabled,
value = formState.value.outputVibra,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputVibra = it } },
onValueChanged = { formState.value = formState.value.copy { outputVibra = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.output_duration_milliseconds),
value = externalNotificationInput.outputMs,
enabled = enabled,
value = formState.value.outputMs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { outputMs = it } },
onValueChanged = { formState.value = formState.value.copy { outputMs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.nag_timeout_seconds),
value = externalNotificationInput.nagTimeout,
enabled = enabled,
value = formState.value.nagTimeout,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { externalNotificationInput = externalNotificationInput.copy { nagTimeout = it } },
onValueChanged = { formState.value = formState.value.copy { nagTimeout = it } },
)
}
@ -243,7 +227,7 @@ fun ExternalNotificationConfigItemList(
title = stringResource(R.string.ringtone),
value = ringtoneInput,
maxSize = 230, // ringtone max_size:231
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
@ -255,39 +239,11 @@ fun ExternalNotificationConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.use_i2s_as_buzzer),
checked = externalNotificationInput.useI2SAsBuzzer,
enabled = enabled,
onCheckedChange = {
externalNotificationInput = externalNotificationInput.copy { useI2SAsBuzzer = it }
},
checked = formState.value.useI2SAsBuzzer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { useI2SAsBuzzer = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && externalNotificationInput != extNotificationConfig || ringtoneInput != ringtone,
onCancelClicked = {
focusManager.clearFocus()
ringtoneInput = ringtone
externalNotificationInput = extNotificationConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(ringtoneInput, externalNotificationInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun ExternalNotificationConfigPreview() {
ExternalNotificationConfigItemList(
ringtone = "",
extNotificationConfig = ExternalNotificationConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
)
}

View file

@ -17,30 +17,24 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ChannelProtos.ChannelSettings
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.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@ -50,73 +44,61 @@ import org.meshtastic.core.model.numChannels
import org.meshtastic.core.strings.R
@Composable
fun LoRaConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val loraConfig = state.radioConfig.lora
val primarySettings = state.channelList.getOrNull(0) ?: return
val formState = rememberConfigState(initialValue = loraConfig)
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
val primaryChannel by remember(formState.value) { mutableStateOf(Channel(primarySettings, formState.value)) }
val focusManager = LocalFocusManager.current
LoRaConfigItemList(
loraConfig = state.radioConfig.lora,
primarySettings = state.channelList.getOrNull(0) ?: return,
RadioConfigScreenList(
title = stringResource(id = R.string.lora),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { loraInput ->
val config = config { lora = loraInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { lora = it }
viewModel.setConfig(config)
},
hasPaFan = viewModel.hasPaFan,
)
}
@Suppress("LongMethod")
@Composable
fun LoRaConfigItemList(
loraConfig: LoRaConfig,
primarySettings: ChannelSettings,
enabled: Boolean,
onSaveClicked: (LoRaConfig) -> Unit,
hasPaFan: Boolean = false,
) {
val focusManager = LocalFocusManager.current
var loraInput by rememberSaveable { mutableStateOf(loraConfig) }
val primaryChannel by remember(loraInput) { mutableStateOf(Channel(primarySettings, loraInput)) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
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 = enabled,
enabled = state.connected,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } },
selectedItem = formState.value.region,
onItemSelected = { formState.value = formState.value.copy { region = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_modem_preset),
checked = loraInput.usePreset,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } },
checked = formState.value.usePreset,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
)
}
item { HorizontalDivider() }
if (loraInput.usePreset) {
if (formState.value.usePreset) {
item {
DropDownPreference(
title = stringResource(R.string.modem_preset),
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
enabled = enabled && loraInput.usePreset,
enabled = state.connected && formState.value.usePreset,
items =
LoRaConfig.ModemPreset.entries
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
.map { it to it.name },
selectedItem = loraInput.modemPreset,
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } },
selectedItem = formState.value.modemPreset,
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
)
}
item { HorizontalDivider() }
@ -124,30 +106,30 @@ fun LoRaConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.bandwidth),
value = loraInput.bandwidth,
enabled = enabled && !loraInput.usePreset,
value = formState.value.bandwidth,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { bandwidth = it } },
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.spread_factor),
value = loraInput.spreadFactor,
enabled = enabled && !loraInput.usePreset,
value = formState.value.spreadFactor,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } },
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.coding_rate),
value = loraInput.codingRate,
enabled = enabled && !loraInput.usePreset,
value = formState.value.codingRate,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { codingRate = it } },
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
)
}
}
@ -156,18 +138,18 @@ fun LoRaConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.ignore_mqtt),
checked = loraInput.ignoreMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } },
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 = loraInput.configOkToMqtt,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } },
checked = formState.value.configOkToMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
)
}
item { HorizontalDivider() }
@ -175,9 +157,9 @@ fun LoRaConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.tx_enabled),
checked = loraInput.txEnabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } },
checked = formState.value.txEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
)
}
item { HorizontalDivider() }
@ -185,10 +167,10 @@ fun LoRaConfigItemList(
EditTextPreference(
title = stringResource(R.string.hop_limit),
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
value = loraInput.hopLimit,
enabled = enabled,
value = formState.value.hopLimit,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } },
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
)
}
item { HorizontalDivider() }
@ -198,13 +180,18 @@ fun LoRaConfigItemList(
EditTextPreference(
title = stringResource(R.string.frequency_slot),
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum,
enabled = enabled,
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 <= loraInput.numChannels) { // total num of LoRa channels
loraInput = loraInput.copy { channelNum = it }
if (it <= formState.value.numChannels) { // total num of LoRa channels
formState.value = formState.value.copy { channelNum = it }
}
},
)
@ -213,9 +200,9 @@ fun LoRaConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.sx126x_rx_boosted_gain),
checked = loraInput.sx126XRxBoostedGain,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } },
checked = formState.value.sx126XRxBoostedGain,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
)
}
item { HorizontalDivider() }
@ -224,63 +211,38 @@ fun LoRaConfigItemList(
EditTextPreference(
title = stringResource(R.string.override_frequency_mhz),
value =
if (isFocused || loraInput.overrideFrequency != 0f) {
loraInput.overrideFrequency
if (isFocused || formState.value.overrideFrequency != 0f) {
formState.value.overrideFrequency
} else {
primaryChannel.radioFreq
},
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } },
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
)
}
item { HorizontalDivider() }
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = loraInput.txPower,
enabled = enabled,
value = formState.value.txPower,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { loraInput = loraInput.copy { txPower = it } },
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
)
}
if (hasPaFan) {
if (viewModel.hasPaFan) {
item {
SwitchPreference(
title = stringResource(R.string.pa_fan_disabled),
checked = loraInput.paFanDisabled,
enabled = enabled,
onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } },
checked = formState.value.paFanDisabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
)
}
item { HorizontalDivider() }
}
item {
PreferenceFooter(
enabled = enabled && loraInput != loraConfig,
onCancelClicked = {
focusManager.clearFocus()
loraInput = loraConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(loraInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun LoRaConfigPreview() {
LoRaConfigItemList(
loraConfig = Channel.default.loraConfig,
primarySettings = Channel.default.settings,
enabled = true,
onSaveClicked = {},
)
}

View file

@ -19,83 +19,72 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditPasswordPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun MQTTConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
val destNum = destNode?.num
val mqttConfig = state.moduleConfig.mqtt
val formState = rememberConfigState(initialValue = mqttConfig)
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
if (!formState.value.mapReportSettings.shouldReportLocation) {
val settings =
formState.value.mapReportSettings.copy {
this.shouldReportLocation = viewModel.shouldReportLocation(destNum)
}
formState.value = formState.value.copy { mapReportSettings = settings }
}
MQTTConfigItemList(
mqttConfig = state.moduleConfig.mqtt,
enabled = state.connected,
shouldReportLocation = viewModel.shouldReportLocation(destNum),
onShouldReportLocationChanged = { shouldReportLocation ->
viewModel.setShouldReportLocation(destNum, shouldReportLocation)
},
onSaveClicked = { mqttInput ->
val config = moduleConfig { mqtt = mqttInput }
val consentValid =
if (formState.value.mapReportingEnabled) {
formState.value.mapReportSettings.shouldReportLocation &&
mqttConfig.mapReportSettings.publishIntervalSecs >= MIN_INTERVAL_SECS
} else {
true
}
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.mqtt),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected && consentValid,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { mqtt = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun MQTTConfigItemList(
mqttConfig: MQTTConfig,
enabled: Boolean,
shouldReportLocation: Boolean,
onShouldReportLocationChanged: (shouldReportLocation: Boolean) -> Unit,
onSaveClicked: (MQTTConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) }
if (!mqttInput.mapReportSettings.shouldReportLocation) {
val settings = mqttInput.mapReportSettings.copy { this.shouldReportLocation = shouldReportLocation }
mqttInput = mqttInput.copy { mapReportSettings = settings }
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.mqtt_config)) }
item {
SwitchPreference(
title = stringResource(R.string.mqtt_enabled),
checked = mqttInput.enabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -103,48 +92,48 @@ fun MQTTConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.address),
value = mqttInput.address,
value = formState.value.address,
maxSize = 63, // address max_size:64
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { address = it } },
onValueChanged = { formState.value = formState.value.copy { address = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.username),
value = mqttInput.username,
value = formState.value.username,
maxSize = 63, // username max_size:64
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { username = it } },
onValueChanged = { formState.value = formState.value.copy { username = it } },
)
}
item {
EditPasswordPreference(
title = stringResource(R.string.password),
value = mqttInput.password,
value = formState.value.password,
maxSize = 63, // password max_size:64
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { password = it } },
onValueChanged = { formState.value = formState.value.copy { password = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.encryption_enabled),
checked = mqttInput.encryptionEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { encryptionEnabled = it } },
checked = formState.value.encryptionEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { encryptionEnabled = it } },
)
}
item { HorizontalDivider() }
@ -152,22 +141,22 @@ fun MQTTConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.json_output_enabled),
checked = mqttInput.jsonEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { jsonEnabled = it } },
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 = mqttInput.address.isEmpty() || mqttInput.address.contains(defaultAddress)
val enforceTls = isDefault && mqttInput.proxyToClientEnabled
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 = mqttInput.tlsEnabled || enforceTls,
enabled = enabled && !enforceTls,
onCheckedChange = { mqttInput = mqttInput.copy { tlsEnabled = it } },
checked = formState.value.tlsEnabled || enforceTls,
enabled = state.connected && !enforceTls,
onCheckedChange = { formState.value = formState.value.copy { tlsEnabled = it } },
)
}
item { HorizontalDivider() }
@ -175,23 +164,23 @@ fun MQTTConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.root_topic),
value = mqttInput.root,
value = formState.value.root,
maxSize = 31, // root max_size:32
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { mqttInput = mqttInput.copy { root = it } },
onValueChanged = { formState.value = formState.value.copy { root = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.proxy_to_client_enabled),
checked = mqttInput.proxyToClientEnabled,
enabled = enabled,
onCheckedChange = { mqttInput = mqttInput.copy { proxyToClientEnabled = it } },
checked = formState.value.proxyToClientEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { proxyToClientEnabled = it } },
)
}
item { HorizontalDivider() }
@ -200,63 +189,30 @@ fun MQTTConfigItemList(
item {
MapReportingPreference(
mapReportingEnabled = mqttInput.mapReportingEnabled,
onMapReportingEnabledChanged = { mqttInput = mqttInput.copy { mapReportingEnabled = it } },
shouldReportLocation = mqttInput.mapReportSettings.shouldReportLocation,
mapReportingEnabled = formState.value.mapReportingEnabled,
onMapReportingEnabledChanged = { formState.value = formState.value.copy { mapReportingEnabled = it } },
shouldReportLocation = formState.value.mapReportSettings.shouldReportLocation,
onShouldReportLocationChanged = {
onShouldReportLocationChanged(it)
val settings = mqttInput.mapReportSettings.copy { this.shouldReportLocation = it }
mqttInput = mqttInput.copy { mapReportSettings = settings }
viewModel.setShouldReportLocation(destNum, it)
val settings = formState.value.mapReportSettings.copy { this.shouldReportLocation = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
positionPrecision = mqttInput.mapReportSettings.positionPrecision,
positionPrecision = formState.value.mapReportSettings.positionPrecision,
onPositionPrecisionChanged = {
val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
mqttInput = mqttInput.copy { mapReportSettings = settings }
val settings = formState.value.mapReportSettings.copy { positionPrecision = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
publishIntervalSecs = mqttInput.mapReportSettings.publishIntervalSecs,
publishIntervalSecs = formState.value.mapReportSettings.publishIntervalSecs,
onPublishIntervalSecsChanged = {
val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it }
mqttInput = mqttInput.copy { mapReportSettings = settings }
val settings = formState.value.mapReportSettings.copy { publishIntervalSecs = it }
formState.value = formState.value.copy { mapReportSettings = settings }
},
enabled = enabled,
enabled = state.connected,
focusManager = focusManager,
)
}
item { HorizontalDivider() }
item {
val consentValid =
if (mqttInput.mapReportingEnabled) {
mqttInput.mapReportSettings.shouldReportLocation &&
mqttConfig.mapReportSettings.publishIntervalSecs >= MIN_INTERVAL_SECS
} else {
true
}
PreferenceFooter(
enabled = enabled && mqttInput != mqttConfig && consentValid,
onCancelClicked = {
focusManager.clearFocus()
mqttInput = mqttConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(mqttInput)
},
)
}
}
}
private const val MIN_INTERVAL_SECS = 3600
@Preview(showBackground = true)
@Composable
private fun MQTTConfigPreview() {
MQTTConfigItemList(
mqttConfig = MQTTConfig.getDefaultInstance(),
enabled = true,
shouldReportLocation = true,
onShouldReportLocationChanged = { _ -> },
onSaveClicked = {},
)
}

View file

@ -17,67 +17,50 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun NeighborInfoConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val neighborInfoConfig = state.moduleConfig.neighborInfo
val formState = rememberConfigState(initialValue = neighborInfoConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
NeighborInfoConfigItemList(
neighborInfoConfig = state.moduleConfig.neighborInfo,
RadioConfigScreenList(
title = stringResource(id = R.string.neighbor_info),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { neighborInfoInput ->
val config = moduleConfig { neighborInfo = neighborInfoInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { neighborInfo = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun NeighborInfoConfigItemList(
neighborInfoConfig: ModuleConfigProtos.ModuleConfig.NeighborInfoConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.NeighborInfoConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var neighborInfoInput by rememberSaveable { mutableStateOf(neighborInfoConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.neighbor_info_config)) }
item {
SwitchPreference(
title = stringResource(R.string.neighbor_info_enabled),
checked = neighborInfoInput.enabled,
enabled = enabled,
onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -85,10 +68,10 @@ fun NeighborInfoConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.update_interval_seconds),
value = neighborInfoInput.updateInterval,
enabled = enabled,
value = formState.value.updateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { neighborInfoInput = neighborInfoInput.copy { updateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { updateInterval = it } },
)
}
@ -96,35 +79,11 @@ fun NeighborInfoConfigItemList(
SwitchPreference(
title = stringResource(R.string.transmit_over_lora),
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
checked = neighborInfoInput.transmitOverLora,
enabled = enabled,
onCheckedChange = { neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it } },
checked = formState.value.transmitOverLora,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { transmitOverLora = it } },
)
HorizontalDivider()
}
item {
PreferenceFooter(
enabled = enabled && neighborInfoInput != neighborInfoConfig,
onCancelClicked = {
focusManager.clearFocus()
neighborInfoInput = neighborInfoConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(neighborInfoInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun NeighborInfoConfigPreview() {
NeighborInfoConfigItemList(
neighborInfoConfig = ModuleConfigProtos.ModuleConfig.NeighborInfoConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -18,11 +18,9 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -38,10 +36,10 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
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.NetworkConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
@ -50,7 +48,6 @@ import com.geeksville.mesh.ui.common.components.EditIPv4Preference
import com.geeksville.mesh.ui.common.components.EditPasswordPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@ -63,40 +60,10 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
SimpleAlertDialog(title = R.string.error, text = R.string.wifi_qr_code_error, onDismiss = onDismiss)
@Composable
fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun NetworkConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
NetworkConfigItemList(
hasWifi = state.metadata?.hasWifi ?: true,
hasEthernet = state.metadata?.hasEthernet ?: true,
networkConfig = state.radioConfig.network,
enabled = state.connected,
onSaveClicked = { networkInput ->
val config = config { network = networkInput }
viewModel.setConfig(config)
},
)
}
private fun extractWifiCredentials(qrCode: String) =
Regex("""WIFI:S:(.*?);.*?P:(.*?);""").find(qrCode)?.destructured?.let { (ssid, password) -> ssid to password }
?: (null to null)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun NetworkConfigItemList(
hasWifi: Boolean,
hasEthernet: Boolean,
networkConfig: NetworkConfig,
enabled: Boolean,
onSaveClicked: (NetworkConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var networkInput by rememberSaveable { mutableStateOf(networkConfig) }
val networkConfig = state.radioConfig.network
val formState = rememberConfigState(initialValue = networkConfig)
var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) }
if (showScanErrorDialog) {
@ -108,8 +75,8 @@ fun NetworkConfigItemList(
if (result.contents != null) {
val (ssid, psk) = extractWifiCredentials(result.contents)
if (ssid != null && psk != null) {
networkInput =
networkInput.copy {
formState.value =
formState.value.copy {
wifiSsid = ssid
wifiPsk = psk
}
@ -129,17 +96,29 @@ fun NetworkConfigItemList(
}
barcodeLauncher.launch(zxingScan)
}
val focusManager = LocalFocusManager.current
LazyColumn(modifier = Modifier.fillMaxSize()) {
if (hasWifi) {
RadioConfigScreenList(
title = stringResource(id = R.string.network),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { network = it }
viewModel.setConfig(config)
},
) {
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 = networkInput.wifiEnabled,
enabled = enabled && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } },
checked = formState.value.wifiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { wifiEnabled = it } },
)
HorizontalDivider()
}
@ -147,25 +126,25 @@ fun NetworkConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.ssid),
value = networkInput.wifiSsid,
value = formState.value.wifiSsid,
maxSize = 32, // wifi_ssid max_size:33
enabled = enabled && hasWifi,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } },
onValueChanged = { formState.value = formState.value.copy { wifiSsid = it } },
)
}
item {
EditPasswordPreference(
title = stringResource(R.string.password),
value = networkInput.wifiPsk,
value = formState.value.wifiPsk,
maxSize = 64, // wifi_psk max_size:65
enabled = enabled && hasWifi,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } },
onValueChanged = { formState.value = formState.value.copy { wifiPsk = it } },
)
}
@ -173,37 +152,38 @@ fun NetworkConfigItemList(
Button(
onClick = { zxingScan() },
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp),
enabled = enabled && hasWifi,
enabled = state.connected,
) {
Text(text = stringResource(R.string.wifi_qr_code_scan))
}
}
}
if (hasEthernet) {
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 = networkInput.ethEnabled,
enabled = enabled && hasEthernet,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } },
checked = formState.value.ethEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ethEnabled = it } },
)
HorizontalDivider()
}
}
if (hasEthernet || hasWifi) {
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 = networkInput.enabledProtocols == 1,
enabled = enabled,
checked = formState.value.enabledProtocols == 1,
enabled = state.connected,
onCheckedChange = {
networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
formState.value =
formState.value.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 }
},
)
}
@ -215,41 +195,41 @@ fun NetworkConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.ntp_server),
value = networkInput.ntpServer,
value = formState.value.ntpServer,
maxSize = 32, // ntp_server max_size:33
enabled = enabled,
isError = networkInput.ntpServer.isEmpty(),
enabled = state.connected,
isError = formState.value.ntpServer.isEmpty(),
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { ntpServer = it } },
onValueChanged = { formState.value = formState.value.copy { ntpServer = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.rsyslog_server),
value = networkInput.rsyslogServer,
value = formState.value.rsyslogServer,
maxSize = 32, // rsyslog_server max_size:33
enabled = enabled,
enabled = state.connected,
isError = false,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { networkInput = networkInput.copy { rsyslogServer = it } },
onValueChanged = { formState.value = formState.value.copy { rsyslogServer = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.ipv4_mode),
enabled = enabled,
enabled = state.connected,
items =
NetworkConfig.AddressMode.entries
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = networkInput.addressMode,
onItemSelected = { networkInput = networkInput.copy { addressMode = it } },
selectedItem = formState.value.addressMode,
onItemSelected = { formState.value = formState.value.copy { addressMode = it } },
)
HorizontalDivider()
}
@ -257,12 +237,12 @@ fun NetworkConfigItemList(
item {
EditIPv4Preference(
title = stringResource(R.string.ip),
value = networkInput.ipv4Config.ip,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
value = formState.value.ipv4Config.ip,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { ip = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
val ipv4 = formState.value.ipv4Config.copy { ip = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
@ -270,12 +250,12 @@ fun NetworkConfigItemList(
item {
EditIPv4Preference(
title = stringResource(R.string.gateway),
value = networkInput.ipv4Config.gateway,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
value = formState.value.ipv4Config.gateway,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { gateway = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
val ipv4 = formState.value.ipv4Config.copy { gateway = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
@ -283,12 +263,12 @@ fun NetworkConfigItemList(
item {
EditIPv4Preference(
title = stringResource(R.string.subnet),
value = networkInput.ipv4Config.subnet,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
value = formState.value.ipv4Config.subnet,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { subnet = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
val ipv4 = formState.value.ipv4Config.copy { subnet = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
@ -296,47 +276,19 @@ fun NetworkConfigItemList(
item {
EditIPv4Preference(
title = "DNS",
value = networkInput.ipv4Config.dns,
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
value = formState.value.ipv4Config.dns,
enabled = state.connected && formState.value.addressMode == NetworkConfig.AddressMode.STATIC,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
val ipv4 = networkInput.ipv4Config.copy { dns = it }
networkInput = networkInput.copy { ipv4Config = ipv4 }
val ipv4 = formState.value.ipv4Config.copy { dns = it }
formState.value = formState.value.copy { ipv4Config = ipv4 }
},
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && networkInput != networkConfig,
onCancelClicked = {
focusManager.clearFocus()
networkInput = networkConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(networkInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun NetworkConfigPreview() {
NetworkConfigItemList(
hasWifi = true,
hasEthernet = true,
networkConfig = NetworkConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}
@Preview(showBackground = true)
@Composable
private fun QrCodeErrorDialogPreview() {
ScanErrorDialog()
}
private fun extractWifiCredentials(qrCode: String) =
Regex("""WIFI:S:(.*?);.*?P:(.*?);""").find(qrCode)?.destructured?.let { (ssid, password) -> ssid to password }
?: (null to null)

View file

@ -17,69 +17,51 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun PaxcounterConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val paxcounterConfig = state.moduleConfig.paxcounter
val formState = rememberConfigState(initialValue = paxcounterConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
PaxcounterConfigItemList(
paxcounterConfig = state.moduleConfig.paxcounter,
RadioConfigScreenList(
title = stringResource(id = R.string.paxcounter),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { paxcounterConfigInput ->
val config = moduleConfig { paxcounter = paxcounterConfigInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { paxcounter = it }
viewModel.setModuleConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun PaxcounterConfigItemList(
paxcounterConfig: ModuleConfigProtos.ModuleConfig.PaxcounterConfig,
enabled: Boolean,
onSaveClicked: (ModuleConfigProtos.ModuleConfig.PaxcounterConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var paxcounterInput by rememberSaveable { mutableStateOf(paxcounterConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.paxcounter_config)) }
item {
SwitchPreference(
title = stringResource(R.string.paxcounter_enabled),
checked = paxcounterInput.enabled,
enabled = enabled,
onCheckedChange = { paxcounterInput = paxcounterInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -87,55 +69,31 @@ fun PaxcounterConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.update_interval_seconds),
value = paxcounterInput.paxcounterUpdateInterval,
enabled = enabled,
value = formState.value.paxcounterUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { paxcounterUpdateInterval = it } },
)
}
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.wifi_rssi_threshold_defaults_to_80),
value = paxcounterInput.wifiThreshold,
enabled = enabled,
value = formState.value.wifiThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { paxcounterInput = paxcounterInput.copy { wifiThreshold = it } },
onValueChanged = { formState.value = formState.value.copy { wifiThreshold = it } },
)
}
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.ble_rssi_threshold_defaults_to_80),
value = paxcounterInput.bleThreshold,
enabled = enabled,
value = formState.value.bleThreshold,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { paxcounterInput = paxcounterInput.copy { bleThreshold = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && paxcounterInput != paxcounterConfig,
onCancelClicked = {
focusManager.clearFocus()
paxcounterInput = paxcounterConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(paxcounterInput)
},
onValueChanged = { formState.value = formState.value.copy { bleThreshold = it } },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PaxcounterConfigPreview() {
PaxcounterConfigItemList(
paxcounterConfig = ModuleConfigProtos.ModuleConfig.PaxcounterConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -21,8 +21,6 @@ import android.Manifest
import android.annotation.SuppressLint
import android.location.Location
import android.os.Build
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
@ -35,13 +33,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.location.LocationCompat
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
import com.geeksville.mesh.Position
@ -51,7 +48,6 @@ import com.geeksville.mesh.ui.common.components.BitwisePreference
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -61,7 +57,7 @@ import org.meshtastic.core.strings.R
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun PositionConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
var phoneLocation: Location? by remember { mutableStateOf(null) }
@ -73,120 +69,104 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
altitude = node?.position?.altitude ?: 0,
time = 1, // ignore time for fixed_position
)
val positionConfig = state.radioConfig.position
val formState = rememberConfigState(initialValue = positionConfig)
var locationInput by rememberSaveable { mutableStateOf(currentPosition) }
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
PositionConfigItemList(
phoneLocation = phoneLocation,
location = currentPosition,
positionConfig = state.radioConfig.position,
enabled = state.connected,
onSaveClicked = { locationInput, positionInput ->
if (positionInput.fixedPosition) {
if (locationInput != currentPosition) {
viewModel.setFixedPosition(locationInput)
}
} else {
if (state.radioConfig.position.fixedPosition) {
// fixed position changed from enabled to disabled
viewModel.removeFixedPosition()
}
}
val config = config { position = positionInput }
viewModel.setConfig(config)
},
onUseCurrentLocation = {
@SuppressLint("MissingPermission")
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
},
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun PositionConfigItemList(
phoneLocation: Location? = null,
location: Position,
positionConfig: PositionConfig,
enabled: Boolean,
onSaveClicked: (position: Position, config: PositionConfig) -> Unit,
onUseCurrentLocation: suspend () -> Unit,
) {
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
val locationPermissionState =
rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) { granted ->
if (granted) {
coroutineScope.launch { onUseCurrentLocation() }
@SuppressLint("MissingPermission")
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
}
}
var locationInput by rememberSaveable { mutableStateOf(location) }
var positionInput by rememberSaveable { mutableStateOf(positionConfig) }
LaunchedEffect(phoneLocation) {
if (phoneLocation != null) {
phoneLocation?.let { phoneLoc ->
locationInput =
Position(
latitude = phoneLocation.latitude,
longitude = phoneLocation.longitude,
latitude = phoneLoc.latitude,
longitude = phoneLoc.longitude,
altitude =
LocationCompat.hasMslAltitude(phoneLocation).let {
LocationCompat.hasMslAltitude(phoneLoc).let {
if (it && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
phoneLocation.mslAltitudeMeters.toInt()
phoneLoc.mslAltitudeMeters.toInt()
} else {
phoneLocation.altitude.toInt()
phoneLoc.altitude.toInt()
}
},
)
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.position),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
if (formState.value.fixedPosition) {
if (locationInput != currentPosition) {
viewModel.setFixedPosition(locationInput)
}
} else {
if (positionConfig.fixedPosition) {
// fixed position changed from enabled to disabled
viewModel.removeFixedPosition()
}
}
val config = config { position = it }
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 = positionInput.positionBroadcastSecs,
enabled = enabled,
value = formState.value.positionBroadcastSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { positionBroadcastSecs = it } },
onValueChanged = { formState.value = formState.value.copy { positionBroadcastSecs = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.smart_position),
checked = positionInput.positionBroadcastSmartEnabled,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { positionBroadcastSmartEnabled = it } },
checked = formState.value.positionBroadcastSmartEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { positionBroadcastSmartEnabled = it } },
)
}
item { HorizontalDivider() }
if (positionInput.positionBroadcastSmartEnabled) {
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 = positionInput.broadcastSmartMinimumIntervalSecs,
enabled = enabled,
value = formState.value.broadcastSmartMinimumIntervalSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it } },
onValueChanged = {
formState.value = formState.value.copy { broadcastSmartMinimumIntervalSecs = it }
},
)
}
item {
EditTextPreference(
title = stringResource(R.string.minimum_distance),
summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary),
value = positionInput.broadcastSmartMinimumDistance,
enabled = enabled,
value = formState.value.broadcastSmartMinimumDistance,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } },
onValueChanged = { formState.value = formState.value.copy { broadcastSmartMinimumDistance = it } },
)
}
}
@ -194,19 +174,19 @@ fun PositionConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.fixed_position),
checked = positionInput.fixedPosition,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } },
checked = formState.value.fixedPosition,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { fixedPosition = it } },
)
}
item { HorizontalDivider() }
if (positionInput.fixedPosition) {
if (formState.value.fixedPosition) {
item {
EditTextPreference(
title = stringResource(R.string.latitude),
value = locationInput.latitude,
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -90 && value <= 90.0) {
@ -219,7 +199,7 @@ fun PositionConfigItemList(
EditTextPreference(
title = stringResource(R.string.longitude),
value = locationInput.longitude,
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
if (value >= -180 && value <= 180.0) {
@ -232,14 +212,14 @@ fun PositionConfigItemList(
EditTextPreference(
title = stringResource(R.string.altitude),
value = locationInput.altitude,
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) },
)
}
item {
TextButton(
enabled = enabled,
enabled = state.connected,
onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } },
) {
Text(text = stringResource(R.string.position_config_set_fixed_from_phone))
@ -250,13 +230,13 @@ fun PositionConfigItemList(
item {
DropDownPreference(
title = stringResource(R.string.gps_mode),
enabled = enabled,
enabled = state.connected,
items =
ConfigProtos.Config.PositionConfig.GpsMode.entries
.filter { it != ConfigProtos.Config.PositionConfig.GpsMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = positionInput.gpsMode,
onItemSelected = { positionInput = positionInput.copy { gpsMode = it } },
selectedItem = formState.value.gpsMode,
onItemSelected = { formState.value = formState.value.copy { gpsMode = it } },
)
}
item { HorizontalDivider() }
@ -265,10 +245,10 @@ fun PositionConfigItemList(
EditTextPreference(
title = stringResource(R.string.update_interval),
summary = stringResource(id = R.string.config_position_gps_update_interval_summary),
value = positionInput.gpsUpdateInterval,
enabled = enabled,
value = formState.value.gpsUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { gpsUpdateInterval = it } },
)
}
item { PreferenceCategory(text = stringResource(R.string.position_flags)) }
@ -276,15 +256,15 @@ fun PositionConfigItemList(
BitwisePreference(
title = stringResource(R.string.position_flags),
summary = stringResource(id = R.string.config_position_flags_summary),
value = positionInput.positionFlags,
enabled = enabled,
value = formState.value.positionFlags,
enabled = state.connected,
items =
ConfigProtos.Config.PositionConfig.PositionFlags.entries
.filter {
it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED
}
.map { it.number to it.name },
onItemSelected = { positionInput = positionInput.copy { positionFlags = it } },
onItemSelected = { formState.value = formState.value.copy { positionFlags = it } },
)
}
item { HorizontalDivider() }
@ -293,58 +273,31 @@ fun PositionConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.gps_receive_gpio),
value = positionInput.rxGpio,
enabled = enabled,
value = formState.value.rxGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { rxGpio = it } },
onValueChanged = { formState.value = formState.value.copy { rxGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gps_transmit_gpio),
value = positionInput.txGpio,
enabled = enabled,
value = formState.value.txGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { txGpio = it } },
onValueChanged = { formState.value = formState.value.copy { txGpio = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.gps_en_gpio),
value = positionInput.gpsEnGpio,
enabled = enabled,
value = formState.value.gpsEnGpio,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsEnGpio = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && positionInput != positionConfig || locationInput != location,
onCancelClicked = {
focusManager.clearFocus()
locationInput = location
positionInput = positionConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(locationInput, positionInput)
},
onValueChanged = { formState.value = formState.value.copy { gpsEnGpio = it } },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PositionConfigPreview() {
PositionConfigItemList(
location = Position(0.0, 0.0, 0),
positionConfig = PositionConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
onUseCurrentLocation = {},
)
}

View file

@ -17,67 +17,51 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos.Config.PowerConfig
import androidx.navigation.NavController
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun PowerConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val powerConfig = state.radioConfig.power
val formState = rememberConfigState(initialValue = powerConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
PowerConfigItemList(
powerConfig = state.radioConfig.power,
RadioConfigScreenList(
title = stringResource(id = R.string.power),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { powerInput ->
val config = config { power = powerInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { power = it }
viewModel.setConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var powerInput by rememberSaveable { mutableStateOf(powerConfig) }
var shutdownOnPowerLoss by rememberSaveable { mutableStateOf(powerConfig.onBatteryShutdownAfterSecs > 0) }
var adcOverride by rememberSaveable { mutableStateOf(powerConfig.adcMultiplierOverride > 0f) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
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 = powerInput.isPowerSaving,
enabled = enabled,
onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } },
checked = formState.value.isPowerSaving,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isPowerSaving = it } },
)
}
item { HorizontalDivider() }
@ -85,23 +69,22 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke
item {
SwitchPreference(
title = stringResource(R.string.shutdown_on_power_loss),
checked = shutdownOnPowerLoss,
enabled = enabled,
checked = formState.value.onBatteryShutdownAfterSecs > 0,
enabled = state.connected,
onCheckedChange = {
shutdownOnPowerLoss = it
if (!it) powerInput = powerInput.copy { onBatteryShutdownAfterSecs = 0 }
formState.value = formState.value.copy { onBatteryShutdownAfterSecs = if (it) 3600 else 0 }
},
)
}
if (shutdownOnPowerLoss) {
if (formState.value.onBatteryShutdownAfterSecs > 0) {
item {
EditTextPreference(
title = stringResource(R.string.shutdown_on_battery_delay_seconds),
value = powerInput.onBatteryShutdownAfterSecs,
enabled = enabled,
value = formState.value.onBatteryShutdownAfterSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } },
onValueChanged = { formState.value = formState.value.copy { onBatteryShutdownAfterSecs = it } },
)
}
}
@ -111,23 +94,22 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke
item {
SwitchPreference(
title = stringResource(R.string.adc_multiplier_override),
checked = adcOverride,
enabled = enabled,
checked = formState.value.adcMultiplierOverride > 0f,
enabled = state.connected,
onCheckedChange = {
adcOverride = it
if (!it) powerInput = powerInput.copy { adcMultiplierOverride = 0f }
formState.value = formState.value.copy { adcMultiplierOverride = if (it) 1.0f else 0.0f }
},
)
}
if (adcOverride) {
if (formState.value.adcMultiplierOverride > 0f) {
item {
EditTextPreference(
title = stringResource(R.string.adc_multiplier_override_ratio),
value = powerInput.adcMultiplierOverride,
enabled = enabled,
value = formState.value.adcMultiplierOverride,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } },
onValueChanged = { formState.value = formState.value.copy { adcMultiplierOverride = it } },
)
}
}
@ -137,61 +119,41 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke
item {
EditTextPreference(
title = stringResource(R.string.wait_for_bluetooth_duration_seconds),
value = powerInput.waitBluetoothSecs,
enabled = enabled,
value = formState.value.waitBluetoothSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { waitBluetoothSecs = it } },
onValueChanged = { formState.value = formState.value.copy { waitBluetoothSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.super_deep_sleep_duration_seconds),
value = powerInput.sdsSecs,
enabled = enabled,
value = formState.value.sdsSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { sdsSecs = it } },
onValueChanged = { formState.value = formState.value.copy { sdsSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.minimum_wake_time_seconds),
value = powerInput.minWakeSecs,
enabled = enabled,
value = formState.value.minWakeSecs,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { minWakeSecs = it } },
onValueChanged = { formState.value = formState.value.copy { minWakeSecs = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.battery_ina_2xx_i2c_address),
value = powerInput.deviceBatteryInaAddress,
enabled = enabled,
value = formState.value.deviceBatteryInaAddress,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { powerInput = powerInput.copy { deviceBatteryInaAddress = it } },
)
}
item {
PreferenceFooter(
enabled = enabled && powerInput != powerConfig,
onCancelClicked = {
focusManager.clearFocus()
powerInput = powerConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(powerInput)
},
onValueChanged = { formState.value = formState.value.copy { deviceBatteryInaAddress = it } },
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PowerConfigPreview() {
PowerConfigItemList(powerConfig = PowerConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
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.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.settings.radio.ResponseState
import com.google.protobuf.MessageLite
@Composable
fun <T : MessageLite> RadioConfigScreenList(
title: String,
onBack: () -> Unit,
responseState: ResponseState<Any>,
onDismissPacketResponse: () -> Unit,
configState: ConfigState<T>,
enabled: Boolean,
onSave: (T) -> Unit,
content: LazyListScope.() -> Unit,
) {
val focusManager = LocalFocusManager.current
if (responseState.isWaiting()) {
PacketResponseStateDialog(state = responseState, onDismiss = onDismissPacketResponse)
}
Scaffold(
topBar = {
MainAppBar(
title = title,
canNavigateUp = true,
onNavigateUp = onBack,
ourNode = null,
isConnected = false,
showNodeChip = false,
actions = {},
onAction = {},
)
},
) { 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)
},
)
}
}
}
}

View file

@ -17,67 +17,50 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun RangeTestConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val rangeTestConfig = state.moduleConfig.rangeTest
val formState = rememberConfigState(initialValue = rangeTestConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
RangeTestConfigItemList(
rangeTestConfig = state.moduleConfig.rangeTest,
RadioConfigScreenList(
title = stringResource(id = R.string.range_test),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { rangeTestInput ->
val config = moduleConfig { rangeTest = rangeTestInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { rangeTest = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun RangeTestConfigItemList(
rangeTestConfig: RangeTestConfig,
enabled: Boolean,
onSaveClicked: (RangeTestConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var rangeTestInput by rememberSaveable { mutableStateOf(rangeTestConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.range_test_config)) }
item {
SwitchPreference(
title = stringResource(R.string.range_test_enabled),
checked = rangeTestInput.enabled,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -85,41 +68,21 @@ fun RangeTestConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.sender_message_interval_seconds),
value = rangeTestInput.sender,
enabled = enabled,
value = formState.value.sender,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } },
onValueChanged = { formState.value = formState.value.copy { sender = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.save_csv_in_storage_esp32_only),
checked = rangeTestInput.save,
enabled = enabled,
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } },
checked = formState.value.save,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { save = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && rangeTestInput != rangeTestConfig,
onCancelClicked = {
focusManager.clearFocus()
rangeTestInput = rangeTestConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(rangeTestInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun RangeTestConfig() {
RangeTestConfigItemList(rangeTestConfig = RangeTestConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,67 +17,50 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun RemoteHardwareConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val remoteHardwareConfig = state.moduleConfig.remoteHardware
val formState = rememberConfigState(initialValue = remoteHardwareConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
RemoteHardwareConfigItemList(
remoteHardwareConfig = state.moduleConfig.remoteHardware,
RadioConfigScreenList(
title = stringResource(id = R.string.remote_hardware),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { remoteHardwareInput ->
val config = moduleConfig { remoteHardware = remoteHardwareInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { remoteHardware = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun RemoteHardwareConfigItemList(
remoteHardwareConfig: RemoteHardwareConfig,
enabled: Boolean,
onSaveClicked: (RemoteHardwareConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var remoteHardwareInput by rememberSaveable { mutableStateOf(remoteHardwareConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.remote_hardware_config)) }
item {
SwitchPreference(
title = stringResource(R.string.remote_hardware_enabled),
checked = remoteHardwareInput.enabled,
enabled = enabled,
onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -85,9 +68,9 @@ fun RemoteHardwareConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.allow_undefined_pin_access),
checked = remoteHardwareInput.allowUndefinedPinAccess,
enabled = enabled,
onCheckedChange = { remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it } },
checked = formState.value.allowUndefinedPinAccess,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { allowUndefinedPinAccess = it } },
)
}
item { HorizontalDivider() }
@ -95,42 +78,18 @@ fun RemoteHardwareConfigItemList(
item {
EditListPreference(
title = stringResource(R.string.available_pins),
list = remoteHardwareInput.availablePinsList,
list = formState.value.availablePinsList,
maxCount = 4, // available_pins max_count:4
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = { list ->
remoteHardwareInput =
remoteHardwareInput.copy {
formState.value =
formState.value.copy {
availablePins.clear()
availablePins.addAll(list)
}
},
)
}
item {
PreferenceFooter(
enabled = enabled && remoteHardwareInput != remoteHardwareConfig,
onCancelClicked = {
focusManager.clearFocus()
remoteHardwareInput = remoteHardwareConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(remoteHardwareInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun RemoteHardwareConfigPreview() {
RemoteHardwareConfigItemList(
remoteHardwareConfig = RemoteHardwareConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -19,12 +19,9 @@ package com.geeksville.mesh.ui.settings.radio.components
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Warning
@ -42,19 +39,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.SecurityConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.CopyIconButton
import com.geeksville.mesh.ui.common.components.EditBase64Preference
import com.geeksville.mesh.ui.common.components.EditListPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.node.NodeActionButton
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@ -64,45 +59,19 @@ import com.google.protobuf.ByteString
import org.meshtastic.core.strings.R
import java.security.SecureRandom
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun SecurityConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val node by viewModel.destNode.collectAsStateWithLifecycle()
val securityConfig = state.radioConfig.security
val formState = rememberConfigState(initialValue = securityConfig)
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
SecurityConfigItemList(
user = node?.user,
securityConfig = state.radioConfig.security,
enabled = state.connected,
onConfirm = { securityInput ->
val config = config { security = securityInput }
viewModel.setConfig(config)
},
onExport = { uri, securityConfig -> viewModel.exportSecurityConfig(uri, securityConfig) },
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod")
@Composable
fun SecurityConfigItemList(
user: MeshProtos.User? = null,
securityConfig: SecurityConfig,
enabled: Boolean,
onConfirm: (config: SecurityConfig) -> Unit,
onExport: (uri: Uri, securityConfig: SecurityConfig) -> Unit = { _, _ -> },
) {
val focusManager = LocalFocusManager.current
var securityInput by rememberSaveable { mutableStateOf(securityConfig) }
var publicKey by rememberSaveable { mutableStateOf(securityInput.publicKey) }
LaunchedEffect(securityInput.privateKey) {
if (securityInput.privateKey != securityConfig.privateKey) {
var publicKey by rememberSaveable { mutableStateOf(formState.value.publicKey) }
LaunchedEffect(formState.value.privateKey) {
if (formState.value.privateKey != securityConfig.privateKey) {
publicKey = "".toByteString()
} else if (securityInput.privateKey == securityConfig.privateKey) {
} else if (formState.value.privateKey == securityConfig.privateKey) {
publicKey = securityConfig.publicKey
}
}
@ -110,18 +79,18 @@ fun SecurityConfigItemList(
val exportConfigLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> onExport(uri, securityConfig) }
it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri, securityConfig) }
}
}
var showKeyGenerationDialog by rememberSaveable { mutableStateOf(false) }
PrivateKeyRegenerateDialog(
showKeyGenerationDialog = showKeyGenerationDialog,
config = securityInput,
onConfirm = { newConfig ->
securityInput = newConfig
onConfirm = {
formState.value = it
showKeyGenerationDialog = false
onConfirm(securityInput)
val config = config { security = formState.value }
viewModel.setConfig(config)
},
onDismiss = { showKeyGenerationDialog = false },
)
@ -141,7 +110,7 @@ fun SecurityConfigItemList(
type = "application/*"
putExtra(
Intent.EXTRA_TITLE,
"${user?.shortName}_keys_${System.currentTimeMillis()}.json",
"${node?.user?.shortName}_keys_${System.currentTimeMillis()}.json",
)
}
exportConfigLauncher.launch(intent)
@ -153,7 +122,19 @@ fun SecurityConfigItemList(
)
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.security),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = config { security = it }
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.direct_message_key)) }
item {
@ -161,15 +142,15 @@ fun SecurityConfigItemList(
title = stringResource(R.string.public_key),
summary = stringResource(id = R.string.config_security_public_key),
value = publicKey,
enabled = enabled,
enabled = state.connected,
readOnly = true,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
securityInput = securityInput.copy { this.publicKey = it }
formState.value = formState.value.copy { this.publicKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = securityInput.publicKey.encodeToString()) },
trailingIcon = { CopyIconButton(valueToCopy = formState.value.publicKey.encodeToString()) },
)
}
@ -177,15 +158,15 @@ fun SecurityConfigItemList(
EditBase64Preference(
title = stringResource(R.string.private_key),
summary = stringResource(id = R.string.config_security_private_key),
value = securityInput.privateKey,
enabled = enabled,
value = formState.value.privateKey,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChange = {
if (it.size() == 32) {
securityInput = securityInput.copy { privateKey = it }
formState.value = formState.value.copy { privateKey = it }
}
},
trailingIcon = { CopyIconButton(valueToCopy = securityInput.privateKey.encodeToString()) },
trailingIcon = { CopyIconButton(valueToCopy = formState.value.privateKey.encodeToString()) },
)
}
@ -193,7 +174,7 @@ fun SecurityConfigItemList(
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.regenerate_private_key),
enabled = enabled,
enabled = state.connected,
icon = Icons.TwoTone.Warning,
onClick = { showKeyGenerationDialog = true },
)
@ -203,7 +184,7 @@ fun SecurityConfigItemList(
NodeActionButton(
modifier = Modifier.padding(horizontal = 8.dp),
title = stringResource(R.string.export_keys),
enabled = enabled,
enabled = state.connected,
icon = Icons.TwoTone.Warning,
onClick = { showEditSecurityConfigDialog = true },
)
@ -213,13 +194,13 @@ fun SecurityConfigItemList(
EditListPreference(
title = stringResource(R.string.admin_key),
summary = stringResource(id = R.string.config_security_admin_key),
list = securityInput.adminKeyList,
list = formState.value.adminKeyList,
maxCount = 3,
enabled = enabled,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValuesChanged = {
securityInput =
securityInput.copy {
formState.value =
formState.value.copy {
adminKey.clear()
adminKey.addAll(it)
}
@ -231,9 +212,9 @@ fun SecurityConfigItemList(
SwitchPreference(
title = stringResource(R.string.serial_console),
summary = stringResource(id = R.string.config_security_serial_enabled),
checked = securityInput.serialEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } },
checked = formState.value.serialEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { serialEnabled = it } },
)
}
item { HorizontalDivider() }
@ -242,9 +223,9 @@ fun SecurityConfigItemList(
SwitchPreference(
title = stringResource(R.string.debug_log_api_enabled),
summary = stringResource(id = R.string.config_security_debug_log_api_enabled),
checked = securityInput.debugLogApiEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { debugLogApiEnabled = it } },
checked = formState.value.debugLogApiEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { debugLogApiEnabled = it } },
)
}
item { HorizontalDivider() }
@ -253,9 +234,9 @@ fun SecurityConfigItemList(
SwitchPreference(
title = stringResource(R.string.managed_mode),
summary = stringResource(id = R.string.config_security_is_managed),
checked = securityInput.isManaged,
enabled = enabled && securityInput.adminKeyCount > 0,
onCheckedChange = { securityInput = securityInput.copy { isManaged = it } },
checked = formState.value.isManaged,
enabled = state.connected && formState.value.adminKeyCount > 0,
onCheckedChange = { formState.value = formState.value.copy { isManaged = it } },
)
}
item { HorizontalDivider() }
@ -263,26 +244,12 @@ fun SecurityConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.legacy_admin_channel),
checked = securityInput.adminChannelEnabled,
enabled = enabled,
onCheckedChange = { securityInput = securityInput.copy { adminChannelEnabled = it } },
checked = formState.value.adminChannelEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { adminChannelEnabled = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && securityInput != securityConfig,
onCancelClicked = {
focusManager.clearFocus()
securityInput = securityConfig
},
onSaveClicked = {
focusManager.clearFocus()
onConfirm(securityInput)
},
)
}
}
}
@ -290,11 +257,9 @@ fun SecurityConfigItemList(
@Composable
fun PrivateKeyRegenerateDialog(
showKeyGenerationDialog: Boolean,
config: SecurityConfig,
onConfirm: (SecurityConfig) -> Unit,
onDismiss: () -> Unit = {},
) {
var securityInput by rememberSaveable { mutableStateOf(config) }
if (showKeyGenerationDialog) {
AlertDialog(
onDismissRequest = onDismiss,
@ -303,20 +268,22 @@ fun PrivateKeyRegenerateDialog(
confirmButton = {
TextButton(
onClick = {
securityInput =
securityInput.copy {
clearPrivateKey()
clearPublicKey()
// Generate a random "f" value
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
privateKey = ByteString.copyFrom(f)
}
val securityInput =
SecurityConfig.newBuilder()
.apply {
clearPrivateKey()
clearPublicKey()
// Generate a random "f" value
val f = ByteArray(32).apply { SecureRandom().nextBytes(this) }
// Adjust the value to make it valid as an "s" value for eval().
// According to the specification we need to mask off the 3
// right-most bits of f[0], mask off the left-most bit of f[31],
// and set the second to left-most bit of f[31].
f[0] = (f[0].toInt() and 0xF8).toByte()
f[31] = ((f[31].toInt() and 0x7F) or 0x40).toByte()
privateKey = ByteString.copyFrom(f)
}
.build()
onConfirm(securityInput)
},
) {
@ -327,9 +294,3 @@ fun PrivateKeyRegenerateDialog(
)
}
}
@Preview(showBackground = true)
@Composable
private fun SecurityConfigPreview() {
SecurityConfigItemList(securityConfig = SecurityConfig.getDefaultInstance(), enabled = true, onConfirm = {})
}

View file

@ -17,65 +17,52 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun SerialConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val serialConfig = state.moduleConfig.serial
val formState = rememberConfigState(initialValue = serialConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
SerialConfigItemList(
serialConfig = state.moduleConfig.serial,
RadioConfigScreenList(
title = stringResource(id = R.string.serial),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { serialInput ->
val config = moduleConfig { serial = serialInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { serial = it }
viewModel.setModuleConfig(config)
},
)
}
@Suppress("LongMethod")
@Composable
fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit) {
val focusManager = LocalFocusManager.current
var serialInput by rememberSaveable { mutableStateOf(serialConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.serial_config)) }
item {
SwitchPreference(
title = stringResource(R.string.serial_enabled),
checked = serialInput.enabled,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -83,9 +70,9 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli
item {
SwitchPreference(
title = stringResource(R.string.echo_enabled),
checked = serialInput.echo,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { echo = it } },
checked = formState.value.echo,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { echo = it } },
)
}
item { HorizontalDivider() }
@ -93,33 +80,33 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli
item {
EditTextPreference(
title = "RX",
value = serialInput.rxd,
enabled = enabled,
value = formState.value.rxd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { rxd = it } },
onValueChanged = { formState.value = formState.value.copy { rxd = it } },
)
}
item {
EditTextPreference(
title = "TX",
value = serialInput.txd,
enabled = enabled,
value = formState.value.txd,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { txd = it } },
onValueChanged = { formState.value = formState.value.copy { txd = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.serial_baud_rate),
enabled = enabled,
enabled = state.connected,
items =
SerialConfig.Serial_Baud.entries
.filter { it != SerialConfig.Serial_Baud.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.baud,
onItemSelected = { serialInput = serialInput.copy { baud = it } },
selectedItem = formState.value.baud,
onItemSelected = { formState.value = formState.value.copy { baud = it } },
)
}
item { HorizontalDivider() }
@ -127,23 +114,23 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli
item {
EditTextPreference(
title = stringResource(R.string.timeout),
value = serialInput.timeout,
enabled = enabled,
value = formState.value.timeout,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { serialInput = serialInput.copy { timeout = it } },
onValueChanged = { formState.value = formState.value.copy { timeout = it } },
)
}
item {
DropDownPreference(
title = stringResource(R.string.serial_mode),
enabled = enabled,
enabled = state.connected,
items =
SerialConfig.Serial_Mode.entries
.filter { it != SerialConfig.Serial_Mode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = serialInput.mode,
onItemSelected = { serialInput = serialInput.copy { mode = it } },
selectedItem = formState.value.mode,
onItemSelected = { formState.value = formState.value.copy { mode = it } },
)
}
item { HorizontalDivider() }
@ -151,31 +138,11 @@ fun SerialConfigItemList(serialConfig: SerialConfig, enabled: Boolean, onSaveCli
item {
SwitchPreference(
title = stringResource(R.string.override_console_serial_port),
checked = serialInput.overrideConsoleSerialPort,
enabled = enabled,
onCheckedChange = { serialInput = serialInput.copy { overrideConsoleSerialPort = it } },
checked = formState.value.overrideConsoleSerialPort,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { overrideConsoleSerialPort = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && serialInput != serialConfig,
onCancelClicked = {
focusManager.clearFocus()
serialInput = serialConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(serialInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SerialConfigPreview() {
SerialConfigItemList(serialConfig = SerialConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,67 +17,50 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun StoreForwardConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val storeForwardConfig = state.moduleConfig.storeForward
val formState = rememberConfigState(initialValue = storeForwardConfig)
val focusManager = LocalFocusManager.current
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
StoreForwardConfigItemList(
storeForwardConfig = state.moduleConfig.storeForward,
RadioConfigScreenList(
title = stringResource(id = R.string.store_forward),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { storeForwardInput ->
val config = moduleConfig { storeForward = storeForwardInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { storeForward = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun StoreForwardConfigItemList(
storeForwardConfig: StoreForwardConfig,
enabled: Boolean,
onSaveClicked: (StoreForwardConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var storeForwardInput by rememberSaveable { mutableStateOf(storeForwardConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.store_forward_config)) }
item {
SwitchPreference(
title = stringResource(R.string.store_forward_enabled),
checked = storeForwardInput.enabled,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { this.enabled = it } },
checked = formState.value.enabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { this.enabled = it } },
)
}
item { HorizontalDivider() }
@ -85,9 +68,9 @@ fun StoreForwardConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.heartbeat),
checked = storeForwardInput.heartbeat,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } },
checked = formState.value.heartbeat,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { heartbeat = it } },
)
}
item { HorizontalDivider() }
@ -95,65 +78,41 @@ fun StoreForwardConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.number_of_records),
value = storeForwardInput.records,
enabled = enabled,
value = formState.value.records,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } },
onValueChanged = { formState.value = formState.value.copy { records = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.history_return_max),
value = storeForwardInput.historyReturnMax,
enabled = enabled,
value = formState.value.historyReturnMax,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnMax = it } },
onValueChanged = { formState.value = formState.value.copy { historyReturnMax = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.history_return_window),
value = storeForwardInput.historyReturnWindow,
enabled = enabled,
value = formState.value.historyReturnWindow,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { storeForwardInput = storeForwardInput.copy { historyReturnWindow = it } },
onValueChanged = { formState.value = formState.value.copy { historyReturnWindow = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.server),
checked = storeForwardInput.isServer,
enabled = enabled,
onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } },
checked = formState.value.isServer,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isServer = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && storeForwardInput != storeForwardConfig,
onCancelClicked = {
focusManager.clearFocus()
storeForwardInput = storeForwardConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(storeForwardInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun StoreForwardConfigPreview() {
StoreForwardConfigItemList(
storeForwardConfig = StoreForwardConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = {},
)
}

View file

@ -17,87 +17,70 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.strings.R
@Composable
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
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
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
TelemetryConfigItemList(
telemetryConfig = state.moduleConfig.telemetry,
RadioConfigScreenList(
title = stringResource(id = R.string.telemetry),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected,
onSaveClicked = { telemetryInput ->
val config = moduleConfig { telemetry = telemetryInput }
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = {
val config = moduleConfig { telemetry = it }
viewModel.setModuleConfig(config)
},
)
}
@Composable
fun TelemetryConfigItemList(
telemetryConfig: TelemetryConfig,
enabled: Boolean,
onSaveClicked: (TelemetryConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var telemetryInput by rememberSaveable { mutableStateOf(telemetryConfig) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
) {
item { PreferenceCategory(text = stringResource(R.string.telemetry_config)) }
item {
EditTextPreference(
title = stringResource(R.string.device_metrics_update_interval_seconds),
value = telemetryInput.deviceUpdateInterval,
enabled = enabled,
value = formState.value.deviceUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { telemetryInput = telemetryInput.copy { deviceUpdateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { deviceUpdateInterval = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.environment_metrics_update_interval_seconds),
value = telemetryInput.environmentUpdateInterval,
enabled = enabled,
value = formState.value.environmentUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { telemetryInput = telemetryInput.copy { environmentUpdateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { environmentUpdateInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_module_enabled),
checked = telemetryInput.environmentMeasurementEnabled,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it } },
checked = formState.value.environmentMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
@ -105,9 +88,9 @@ fun TelemetryConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_on_screen_enabled),
checked = telemetryInput.environmentScreenEnabled,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentScreenEnabled = it } },
checked = formState.value.environmentScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentScreenEnabled = it } },
)
}
item { HorizontalDivider() }
@ -115,9 +98,9 @@ fun TelemetryConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.environment_metrics_use_fahrenheit),
checked = telemetryInput.environmentDisplayFahrenheit,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it } },
checked = formState.value.environmentDisplayFahrenheit,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { environmentDisplayFahrenheit = it } },
)
}
item { HorizontalDivider() }
@ -125,9 +108,9 @@ fun TelemetryConfigItemList(
item {
SwitchPreference(
title = stringResource(R.string.air_quality_metrics_module_enabled),
checked = telemetryInput.airQualityEnabled,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { airQualityEnabled = it } },
checked = formState.value.airQualityEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { airQualityEnabled = it } },
)
}
item { HorizontalDivider() }
@ -135,19 +118,19 @@ fun TelemetryConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.air_quality_metrics_update_interval_seconds),
value = telemetryInput.airQualityInterval,
enabled = enabled,
value = formState.value.airQualityInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { telemetryInput = telemetryInput.copy { airQualityInterval = it } },
onValueChanged = { formState.value = formState.value.copy { airQualityInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.power_metrics_module_enabled),
checked = telemetryInput.powerMeasurementEnabled,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it } },
checked = formState.value.powerMeasurementEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerMeasurementEnabled = it } },
)
}
item { HorizontalDivider() }
@ -155,41 +138,21 @@ fun TelemetryConfigItemList(
item {
EditTextPreference(
title = stringResource(R.string.power_metrics_update_interval_seconds),
value = telemetryInput.powerUpdateInterval,
enabled = enabled,
value = formState.value.powerUpdateInterval,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { telemetryInput = telemetryInput.copy { powerUpdateInterval = it } },
onValueChanged = { formState.value = formState.value.copy { powerUpdateInterval = it } },
)
}
item {
SwitchPreference(
title = stringResource(R.string.power_metrics_on_screen_enabled),
checked = telemetryInput.powerScreenEnabled,
enabled = enabled,
onCheckedChange = { telemetryInput = telemetryInput.copy { powerScreenEnabled = it } },
checked = formState.value.powerScreenEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { powerScreenEnabled = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && telemetryInput != telemetryConfig,
onCancelClicked = {
focusManager.clearFocus()
telemetryInput = telemetryConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(telemetryInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun TelemetryConfigPreview() {
TelemetryConfigItemList(telemetryConfig = TelemetryConfig.getDefaultInstance(), enabled = true, onSaveClicked = {})
}

View file

@ -17,32 +17,23 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import androidx.navigation.NavController
import com.geeksville.mesh.copy
import com.geeksville.mesh.deviceMetadata
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.isUnmessageableRole
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.RegularPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@ -50,74 +41,65 @@ import com.geeksville.mesh.user
import org.meshtastic.core.strings.R
@Composable
fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
fun UserConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val userConfig = state.userConfig
val formState = rememberConfigState(initialValue = userConfig)
val firmwareVersion = DeviceVersion(state.metadata?.firmwareVersion ?: "")
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
UserConfigItemList(
userConfig = state.userConfig,
enabled = true,
onSaveClicked = viewModel::setOwner,
metadata = state.metadata,
)
}
@Suppress("LongMethod")
@Composable
fun UserConfigItemList(
metadata: MeshProtos.DeviceMetadata?,
userConfig: MeshProtos.User,
enabled: Boolean,
onSaveClicked: (MeshProtos.User) -> Unit,
) {
val focusManager = LocalFocusManager.current
var userInput by rememberSaveable { mutableStateOf(userConfig) }
val firmwareVersion = DeviceVersion(metadata?.firmwareVersion ?: "")
val validLongName = userInput.longName.isNotBlank()
val validShortName = userInput.shortName.isNotBlank()
val validLongName = formState.value.longName.isNotBlank()
val validShortName = formState.value.shortName.isNotBlank()
val validNames = validLongName && validShortName
LazyColumn(modifier = Modifier.fillMaxSize()) {
val focusManager = LocalFocusManager.current
RadioConfigScreenList(
title = stringResource(id = R.string.user),
onBack = { navController.popBackStack() },
configState = formState,
enabled = state.connected && validNames,
responseState = state.responseState,
onDismissPacketResponse = viewModel::clearPacketResponse,
onSave = viewModel::setOwner,
) {
item { PreferenceCategory(text = stringResource(R.string.user_config)) }
item { RegularPreference(title = stringResource(R.string.node_id), subtitle = userInput.id, onClick = {}) }
item {
RegularPreference(title = stringResource(R.string.node_id), subtitle = formState.value.id, onClick = {})
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.long_name),
value = userInput.longName,
value = formState.value.longName,
maxSize = 39, // long_name max_size:40
enabled = enabled,
enabled = state.connected,
isError = !validLongName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { userInput = userInput.copy { longName = it } },
onValueChanged = { formState.value = formState.value.copy { longName = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.short_name),
value = userInput.shortName,
value = formState.value.shortName,
maxSize = 4, // short_name max_size:5
enabled = enabled,
enabled = state.connected,
isError = !validShortName,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { userInput = userInput.copy { shortName = it } },
onValueChanged = { formState.value = formState.value.copy { shortName = it } },
)
}
item {
RegularPreference(
title = stringResource(R.string.hardware_model),
subtitle = userInput.hwModel.name,
subtitle = formState.value.hwModel.name,
onClick = {},
)
}
@ -128,10 +110,10 @@ fun UserConfigItemList(
title = stringResource(R.string.unmessageable),
summary = stringResource(R.string.unmonitored_or_infrastructure),
checked =
userInput.isUnmessagable ||
(firmwareVersion < DeviceVersion("2.6.9") && userInput.role.isUnmessageableRole()),
enabled = userInput.hasIsUnmessagable() || firmwareVersion >= DeviceVersion("2.6.9"),
onCheckedChange = { userInput = userInput.copy { isUnmessagable = it } },
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 } },
)
}
@ -141,43 +123,11 @@ fun UserConfigItemList(
SwitchPreference(
title = stringResource(R.string.licensed_amateur_radio),
summary = stringResource(R.string.licensed_amateur_radio_text),
checked = userInput.isLicensed,
enabled = enabled,
onCheckedChange = { userInput = userInput.copy { isLicensed = it } },
checked = formState.value.isLicensed,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { isLicensed = it } },
)
}
item { HorizontalDivider() }
item {
PreferenceFooter(
enabled = enabled && userInput != userConfig && validNames,
onCancelClicked = {
focusManager.clearFocus()
userInput = userConfig
},
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(userInput)
},
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun UserConfigPreview() {
UserConfigItemList(
userConfig =
user {
id = "!a280d9c8"
longName = "Meshtastic d9c8"
shortName = "d9c8"
hwModel = MeshProtos.HardwareModel.RAK4631
isLicensed = false
},
enabled = true,
onSaveClicked = {},
metadata = deviceMetadata { firmwareVersion = "2.8.0" },
)
}