mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: move RadioConfig files to separate package
This commit is contained in:
parent
7794c08190
commit
ad9a3a5e49
39 changed files with 501 additions and 357 deletions
|
|
@ -54,7 +54,7 @@ import com.geeksville.mesh.R
|
|||
import com.geeksville.mesh.channelSet
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.ui.components.config.ChannelSelection
|
||||
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
|
||||
|
||||
/**
|
||||
* Enables the user to select which channels to accept after scanning a QR code.
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun AmbientLightingConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
AmbientLightingConfigItemList(
|
||||
ambientLightingConfig = state.moduleConfig.ambientLighting,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { ambientLightingInput ->
|
||||
val config = moduleConfig { ambientLighting = ambientLightingInput }
|
||||
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 = "Ambient Lighting Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "LED state",
|
||||
checked = ambientLightingInput.ledState,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
ambientLightingInput = ambientLightingInput.copy { ledState = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Current",
|
||||
value = ambientLightingInput.current,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
ambientLightingInput = ambientLightingInput.copy { current = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Red",
|
||||
value = ambientLightingInput.red,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { red = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Green",
|
||||
value = ambientLightingInput.green,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { ambientLightingInput = ambientLightingInput.copy { green = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Blue",
|
||||
value = ambientLightingInput.blue,
|
||||
enabled = enabled,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AmbientLightingConfigPreview() {
|
||||
AmbientLightingConfigItemList(
|
||||
ambientLightingConfig = ModuleConfigProtos.ModuleConfig.AmbientLightingConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.AudioConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun AudioConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
AudioConfigItemList(
|
||||
audioConfig = state.moduleConfig.audio,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { audioInput ->
|
||||
val config = moduleConfig { audio = audioInput }
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 = "Audio Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "CODEC 2 enabled",
|
||||
checked = audioInput.codec2Enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { audioInput = audioInput.copy { codec2Enabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "PTT pin",
|
||||
value = audioInput.pttPin,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { audioInput = audioInput.copy { pttPin = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "CODEC2 sample rate",
|
||||
enabled = enabled,
|
||||
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 } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "I2S word select",
|
||||
value = audioInput.i2SWs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { audioInput = audioInput.copy { i2SWs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "I2S data in",
|
||||
value = audioInput.i2SSd,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { audioInput = audioInput.copy { i2SSd = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "I2S data out",
|
||||
value = audioInput.i2SDin,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { audioInput = audioInput.copy { i2SDin = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "I2S clock",
|
||||
value = audioInput.i2SSck,
|
||||
enabled = enabled,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun AudioConfigPreview() {
|
||||
AudioConfigItemList(
|
||||
audioConfig = AudioConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.BluetoothConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun BluetoothConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
BluetoothConfigItemList(
|
||||
bluetoothConfig = state.radioConfig.bluetooth,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { bluetoothInput ->
|
||||
val config = config { bluetooth = bluetoothInput }
|
||||
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 = "Bluetooth Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Bluetooth enabled",
|
||||
checked = bluetoothInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { bluetoothInput = bluetoothInput.copy { this.enabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Pairing mode",
|
||||
enabled = enabled,
|
||||
items = BluetoothConfig.PairingMode.entries
|
||||
.filter { it != BluetoothConfig.PairingMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = bluetoothInput.mode,
|
||||
onItemSelected = { bluetoothInput = bluetoothInput.copy { mode = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Fixed PIN",
|
||||
value = bluetoothInput.fixedPin,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
if (it.toString().length == 6) { // ensure 6 digits
|
||||
bluetoothInput = bluetoothInput.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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Divider
|
||||
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.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.CannedMessageConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun CannedMessageConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
CannedMessageConfigItemList(
|
||||
messages = state.cannedMessageMessages,
|
||||
cannedMessageConfig = state.moduleConfig.cannedMessage,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { messagesInput, cannedMessageInput ->
|
||||
if (messagesInput != state.cannedMessageMessages) {
|
||||
viewModel.setCannedMessages(messagesInput)
|
||||
}
|
||||
if (cannedMessageInput != state.moduleConfig.cannedMessage) {
|
||||
val config = moduleConfig { cannedMessage = cannedMessageInput }
|
||||
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 = "Canned Message Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Canned message enabled",
|
||||
checked = cannedMessageInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
cannedMessageInput = cannedMessageInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Rotary encoder #1 enabled",
|
||||
checked = cannedMessageInput.rotary1Enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
cannedMessageInput = cannedMessageInput.copy { rotary1Enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "GPIO pin for rotary encoder A port",
|
||||
value = cannedMessageInput.inputbrokerPinA,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinA = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "GPIO pin for rotary encoder B port",
|
||||
value = cannedMessageInput.inputbrokerPinB,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinB = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "GPIO pin for rotary encoder Press port",
|
||||
value = cannedMessageInput.inputbrokerPinPress,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerPinPress = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Generate input event on Press",
|
||||
enabled = enabled,
|
||||
items = CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = cannedMessageInput.inputbrokerEventPress,
|
||||
onItemSelected = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventPress = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Generate input event on CW",
|
||||
enabled = enabled,
|
||||
items = CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = cannedMessageInput.inputbrokerEventCw,
|
||||
onItemSelected = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCw = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Generate input event on CCW",
|
||||
enabled = enabled,
|
||||
items = CannedMessageConfig.InputEventChar.entries
|
||||
.filter { it != CannedMessageConfig.InputEventChar.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = cannedMessageInput.inputbrokerEventCcw,
|
||||
onItemSelected = {
|
||||
cannedMessageInput = cannedMessageInput.copy { inputbrokerEventCcw = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Up/Down/Select input enabled",
|
||||
checked = cannedMessageInput.updown1Enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
cannedMessageInput = cannedMessageInput.copy { updown1Enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Allow input source",
|
||||
value = cannedMessageInput.allowInputSource,
|
||||
maxSize = 63, // allow_input_source max_size:16
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
cannedMessageInput = cannedMessageInput.copy { allowInputSource = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Send bell",
|
||||
checked = cannedMessageInput.sendBell,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
cannedMessageInput = cannedMessageInput.copy { sendBell = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Messages",
|
||||
value = messagesInput,
|
||||
maxSize = 200, // messages max_size:201
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
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 = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Chip
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.Add
|
||||
import androidx.compose.material.icons.twotone.Close
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.channelSettings
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.dragContainer
|
||||
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
||||
import com.geeksville.mesh.ui.components.rememberDragDropState
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ChannelItem(
|
||||
index: Int,
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit = {},
|
||||
elevation: Dp = 4.dp,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.clickable(enabled = enabled) { onClick() },
|
||||
elevation = elevation,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||
) {
|
||||
val textColor = if (enabled) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
|
||||
}
|
||||
|
||||
Chip(onClick = onClick) {
|
||||
Text(
|
||||
text = "$index",
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = textColor,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.body1,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelCard(
|
||||
index: Int,
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
elevation: Dp = 4.dp,
|
||||
) = ChannelItem(
|
||||
index = index,
|
||||
title = title,
|
||||
enabled = enabled,
|
||||
onClick = onEditClick,
|
||||
elevation = elevation,
|
||||
) {
|
||||
IconButton(onClick = { onDeleteClick() }) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Close,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelSelection(
|
||||
index: Int,
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
isSelected: Boolean,
|
||||
onSelected: (Boolean) -> Unit
|
||||
) = ChannelItem(
|
||||
index = index,
|
||||
title = title,
|
||||
enabled = enabled,
|
||||
onClick = {},
|
||||
) {
|
||||
Checkbox(
|
||||
enabled = enabled,
|
||||
checked = isSelected,
|
||||
onCheckedChange = onSelected,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
ChannelSettingsItemList(
|
||||
settingsList = state.channelList,
|
||||
modemPresetName = Channel(loraConfig = state.radioConfig.lora).name,
|
||||
enabled = state.connected,
|
||||
maxChannels = viewModel.maxChannels,
|
||||
onPositiveClicked = { channelListInput ->
|
||||
viewModel.updateChannels(channelListInput, state.channelList)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelSettingsItemList(
|
||||
settingsList: List<ChannelSettings>,
|
||||
modemPresetName: String = "Default",
|
||||
maxChannels: Int = 8,
|
||||
enabled: Boolean,
|
||||
onNegativeClicked: () -> Unit = { },
|
||||
onPositiveClicked: (List<ChannelSettings>) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val settingsListInput = rememberSaveable(
|
||||
saver = listSaver(save = { it.toList() }, restore = { it.toMutableStateList() })
|
||||
) { settingsList.toMutableStateList() }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState = rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
|
||||
if (toIndex in settingsListInput.indices && fromIndex in settingsListInput.indices) {
|
||||
settingsListInput.apply { add(toIndex, removeAt(fromIndex)) }
|
||||
}
|
||||
}
|
||||
|
||||
val isEditing: Boolean = settingsList.size != settingsListInput.size ||
|
||||
settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
|
||||
|
||||
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
|
||||
if (showEditChannelDialog != null) {
|
||||
val index = showEditChannelDialog ?: return
|
||||
EditChannelDialog(
|
||||
channelSettings = with(settingsListInput) {
|
||||
if (size > index) get(index) else channelSettings { }
|
||||
},
|
||||
modemPresetName = modemPresetName,
|
||||
onAddClick = {
|
||||
if (settingsListInput.size > index) {
|
||||
settingsListInput[index] = it
|
||||
} else {
|
||||
settingsListInput.add(it)
|
||||
}
|
||||
showEditChannelDialog = null
|
||||
},
|
||||
onDismissRequest = { showEditChannelDialog = null }
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(onClick = { }, enabled = false)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.dragContainer(
|
||||
dragDropState = dragDropState,
|
||||
haptics = LocalHapticFeedback.current,
|
||||
),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
item { PreferenceCategory(text = "Channels") }
|
||||
|
||||
dragDropItemsIndexed(
|
||||
items = settingsListInput,
|
||||
dragDropState = dragDropState,
|
||||
) { index, channel, isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp, label = "drag")
|
||||
ChannelCard(
|
||||
elevation = elevation,
|
||||
index = index,
|
||||
title = channel.name.ifEmpty { modemPresetName },
|
||||
enabled = enabled,
|
||||
onEditClick = { showEditChannelDialog = index },
|
||||
onDeleteClick = { settingsListInput.removeAt(index) }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled && isEditing,
|
||||
negativeText = R.string.cancel,
|
||||
onNegativeClicked = {
|
||||
focusManager.clearFocus()
|
||||
settingsListInput.clear()
|
||||
settingsListInput.addAll(settingsList)
|
||||
onNegativeClicked()
|
||||
},
|
||||
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)
|
||||
)
|
||||
) {
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ChannelSettingsPreview() {
|
||||
ChannelSettingsItemList(
|
||||
settingsList = listOf(
|
||||
channelSettings {
|
||||
psk = Channel.default.settings.psk
|
||||
name = Channel.default.name
|
||||
},
|
||||
channelSettings {
|
||||
name = stringResource(R.string.channel_name)
|
||||
},
|
||||
),
|
||||
enabled = true,
|
||||
onPositiveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Divider
|
||||
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.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun DetectionSensorConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
DetectionSensorConfigItemList(
|
||||
detectionSensorConfig = state.moduleConfig.detectionSensor,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { detectionSensorInput ->
|
||||
val config = moduleConfig { detectionSensor = detectionSensorInput }
|
||||
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 = "Detection Sensor Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Detection Sensor enabled",
|
||||
checked = detectionSensorInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
detectionSensorInput = detectionSensorInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Minimum broadcast (seconds)",
|
||||
value = detectionSensorInput.minimumBroadcastSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
detectionSensorInput = detectionSensorInput.copy { minimumBroadcastSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "State broadcast (seconds)",
|
||||
value = detectionSensorInput.stateBroadcastSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
detectionSensorInput = detectionSensorInput.copy { stateBroadcastSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Send bell with alert message",
|
||||
checked = detectionSensorInput.sendBell,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
detectionSensorInput = detectionSensorInput.copy { sendBell = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Friendly name",
|
||||
value = detectionSensorInput.name,
|
||||
maxSize = 19, // name max_size:20
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
detectionSensorInput = detectionSensorInput.copy { name = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "GPIO pin to monitor",
|
||||
value = detectionSensorInput.monitorPin,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
detectionSensorInput = detectionSensorInput.copy { monitorPin = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(
|
||||
title = "Detection trigger type",
|
||||
enabled = enabled,
|
||||
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 }
|
||||
}
|
||||
)
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Use INPUT_PULLUP mode",
|
||||
checked = detectionSensorInput.usePullup,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
detectionSensorInput = detectionSensorInput.copy { usePullup = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Divider
|
||||
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.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
private val DeviceConfig.Role.stringRes: Int
|
||||
get() = when (this) {
|
||||
DeviceConfig.Role.CLIENT -> R.string.role_client
|
||||
DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute
|
||||
DeviceConfig.Role.ROUTER -> R.string.role_router
|
||||
DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client
|
||||
DeviceConfig.Role.REPEATER -> R.string.role_repeater
|
||||
DeviceConfig.Role.TRACKER -> R.string.role_tracker
|
||||
DeviceConfig.Role.SENSOR -> R.string.role_sensor
|
||||
DeviceConfig.Role.TAK -> R.string.role_tak
|
||||
DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden
|
||||
DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found
|
||||
DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker
|
||||
DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late
|
||||
else -> R.string.unrecognized
|
||||
}
|
||||
|
||||
private val DeviceConfig.RebroadcastMode.stringRes: Int
|
||||
get() = when (this) {
|
||||
DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all
|
||||
DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding
|
||||
DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only
|
||||
DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only
|
||||
DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none
|
||||
DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only
|
||||
else -> R.string.unrecognized
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
DeviceConfigItemList(
|
||||
deviceConfig = state.radioConfig.device,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { deviceInput ->
|
||||
val config = config { device = deviceInput }
|
||||
viewModel.setConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceConfigItemList(
|
||||
deviceConfig: DeviceConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (DeviceConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var deviceInput by rememberSaveable { mutableStateOf(deviceConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "Device Config") }
|
||||
|
||||
item {
|
||||
DropDownPreference(
|
||||
title = "Role",
|
||||
enabled = enabled,
|
||||
selectedItem = deviceInput.role,
|
||||
onItemSelected = { deviceInput = deviceInput.copy { role = it } },
|
||||
summary = stringResource(id = deviceInput.role.stringRes),
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Redefine PIN_BUTTON",
|
||||
value = deviceInput.buttonGpio,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
deviceInput = deviceInput.copy { buttonGpio = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Redefine PIN_BUZZER",
|
||||
value = deviceInput.buzzerGpio,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
deviceInput = deviceInput.copy { buzzerGpio = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(
|
||||
title = "Rebroadcast mode",
|
||||
enabled = enabled,
|
||||
selectedItem = deviceInput.rebroadcastMode,
|
||||
onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } },
|
||||
summary = stringResource(id = deviceInput.rebroadcastMode.stringRes),
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "NodeInfo broadcast interval (seconds)",
|
||||
value = deviceInput.nodeInfoBroadcastSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = "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 } }
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = "Disable triple-click",
|
||||
summary = stringResource(id = R.string.config_device_disableTripleClick_summary),
|
||||
checked = deviceInput.disableTripleClick,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } }
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "POSIX Timezone",
|
||||
value = deviceInput.tzdef,
|
||||
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 {
|
||||
SwitchPreference(
|
||||
title = "Disable LED heartbeat",
|
||||
summary = stringResource(id = R.string.config_device_ledHeartbeatDisabled_summary),
|
||||
checked = deviceInput.ledHeartbeatDisabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } }
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun DisplayConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
DisplayConfigItemList(
|
||||
displayConfig = state.radioConfig.display,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { displayInput ->
|
||||
val config = config { display = displayInput }
|
||||
viewModel.setConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 = "Display Config") }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Screen timeout (seconds)",
|
||||
value = displayInput.screenOnSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "GPS coordinates format",
|
||||
enabled = enabled,
|
||||
items = DisplayConfig.GpsCoordinateFormat.entries
|
||||
.filter { it != DisplayConfig.GpsCoordinateFormat.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = displayInput.gpsFormat,
|
||||
onItemSelected = { displayInput = displayInput.copy { gpsFormat = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Auto screen carousel (seconds)",
|
||||
value = displayInput.autoScreenCarouselSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
displayInput = displayInput.copy { autoScreenCarouselSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Compass north top",
|
||||
checked = displayInput.compassNorthTop,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Flip screen",
|
||||
checked = displayInput.flipScreen,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Display units",
|
||||
enabled = enabled,
|
||||
items = DisplayConfig.DisplayUnits.entries
|
||||
.filter { it != DisplayConfig.DisplayUnits.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = displayInput.units,
|
||||
onItemSelected = { displayInput = displayInput.copy { units = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Override OLED auto-detect",
|
||||
enabled = enabled,
|
||||
items = DisplayConfig.OledType.entries
|
||||
.filter { it != DisplayConfig.OledType.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = displayInput.oled,
|
||||
onItemSelected = { displayInput = displayInput.copy { oled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Display mode",
|
||||
enabled = enabled,
|
||||
items = DisplayConfig.DisplayMode.entries
|
||||
.filter { it != DisplayConfig.DisplayMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = displayInput.displaymode,
|
||||
onItemSelected = { displayInput = displayInput.copy { displaymode = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Heading bold",
|
||||
checked = displayInput.headingBold,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { displayInput = displayInput.copy { headingBold = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Wake screen on tap or motion",
|
||||
checked = displayInput.wakeOnTapOrMotion,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Compass orientation",
|
||||
enabled = enabled,
|
||||
items = DisplayConfig.CompassOrientation.entries
|
||||
.filter { it != DisplayConfig.CompassOrientation.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = displayInput.compassOrientation,
|
||||
onItemSelected = { displayInput = displayInput.copy { compassOrientation = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.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.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.channelSettings
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.ui.components.EditBase64Preference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun EditChannelDialog(
|
||||
channelSettings: ChannelProtos.ChannelSettings,
|
||||
onAddClick: (ChannelProtos.ChannelSettings) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
modemPresetName: String = "Default",
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
var channelInput by remember(channelSettings) { mutableStateOf(channelSettings) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
text = {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.channel_name),
|
||||
value = if (isFocused) channelInput.name else channelInput.name.ifEmpty { modemPresetName },
|
||||
maxSize = 11, // name max_size:12
|
||||
enabled = true,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
channelInput = channelInput.copy {
|
||||
name = it.trim()
|
||||
if (psk == Channel.default.settings.psk) psk = Channel.getRandomKey()
|
||||
}
|
||||
},
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
)
|
||||
|
||||
EditBase64Preference(
|
||||
title = "PSK",
|
||||
value = channelInput.psk,
|
||||
enabled = true,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
val fullPsk = Channel(channelSettings { psk = it }).psk
|
||||
if (fullPsk.size() in setOf(0, 16, 32)) {
|
||||
channelInput = channelInput.copy { psk = it }
|
||||
}
|
||||
},
|
||||
onGenerateKey = {
|
||||
channelInput = channelInput.copy { psk = Channel.getRandomKey() }
|
||||
},
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = "Uplink enabled",
|
||||
checked = channelInput.uplinkEnabled,
|
||||
enabled = true,
|
||||
onCheckedChange = {
|
||||
channelInput = channelInput.copy { uplinkEnabled = it }
|
||||
},
|
||||
padding = PaddingValues(0.dp)
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = "Downlink enabled",
|
||||
checked = channelInput.downlinkEnabled,
|
||||
enabled = true,
|
||||
onCheckedChange = {
|
||||
channelInput = channelInput.copy { downlinkEnabled = it }
|
||||
},
|
||||
padding = PaddingValues(0.dp)
|
||||
)
|
||||
|
||||
PositionPrecisionPreference(
|
||||
title = "Position enabled",
|
||||
enabled = true,
|
||||
value = channelInput.moduleSettings.positionPrecision,
|
||||
onValueChanged = {
|
||||
val module = channelInput.moduleSettings.copy { positionPrecision = it }
|
||||
channelInput = channelInput.copy { moduleSettings = module }
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
FlowRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = onDismissRequest
|
||||
) { Text(stringResource(R.string.cancel)) }
|
||||
Button(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = {
|
||||
onAddClick(channelInput)
|
||||
},
|
||||
enabled = true,
|
||||
) { Text(stringResource(R.string.save)) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun EditChannelDialogPreview() {
|
||||
EditChannelDialog(
|
||||
channelSettings = channelSettings {
|
||||
psk = Channel.default.settings.psk
|
||||
name = Channel.default.name
|
||||
},
|
||||
onAddClick = { },
|
||||
onDismissRequest = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.google.protobuf.Descriptors
|
||||
|
||||
private const val SupportedFields = 7
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun EditDeviceProfileDialog(
|
||||
title: String,
|
||||
deviceProfile: DeviceProfile,
|
||||
onConfirm: (DeviceProfile) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state = remember {
|
||||
val fields = deviceProfile.descriptorForType.fields
|
||||
.filter { it.number < SupportedFields } // TODO add ringtone & canned messages
|
||||
mutableStateMapOf<Descriptors.FieldDescriptor, Boolean>()
|
||||
.apply { putAll(fields.associateWith(deviceProfile::hasField)) }
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
text = {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.h6.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
)
|
||||
Divider()
|
||||
state.keys.sortedBy { it.number }.forEach { field ->
|
||||
SwitchPreference(
|
||||
title = field.name,
|
||||
checked = state[field] == true,
|
||||
enabled = deviceProfile.hasField(field),
|
||||
onCheckedChange = { state[field] = it },
|
||||
padding = PaddingValues(0.dp)
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
FlowRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = onDismiss
|
||||
) { Text(stringResource(R.string.cancel)) }
|
||||
Button(
|
||||
modifier = modifier.weight(1f),
|
||||
onClick = {
|
||||
val builder = DeviceProfile.newBuilder()
|
||||
deviceProfile.allFields.forEach { (field, value) ->
|
||||
if (state[field] == true) {
|
||||
builder.setField(field, value)
|
||||
}
|
||||
}
|
||||
onConfirm(builder.build())
|
||||
},
|
||||
enabled = state.values.any { it },
|
||||
) { Text(stringResource(R.string.save)) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun EditDeviceProfileDialogPreview() {
|
||||
EditDeviceProfileDialog(
|
||||
title = "Export configuration",
|
||||
deviceProfile = DeviceProfile.getDefaultInstance(),
|
||||
onConfirm = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Divider
|
||||
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.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.ExternalNotificationConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.geeksville.mesh.ui.components.TextDividerPreference
|
||||
|
||||
@Composable
|
||||
fun ExternalNotificationConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
ExternalNotificationConfigItemList(
|
||||
ringtone = state.ringtone,
|
||||
extNotificationConfig = state.moduleConfig.externalNotification,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { ringtoneInput, extNotificationInput ->
|
||||
if (ringtoneInput != state.ringtone) {
|
||||
viewModel.setRingtone(ringtoneInput)
|
||||
}
|
||||
if (extNotificationInput != state.moduleConfig.externalNotification) {
|
||||
val config = moduleConfig { externalNotification = extNotificationInput }
|
||||
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 = "External Notification Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "External notification enabled",
|
||||
checked = externalNotificationInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
|
||||
item { TextDividerPreference("Notifications on message receipt", enabled = enabled) }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert message LED",
|
||||
checked = externalNotificationInput.alertMessage,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { alertMessage = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert message buzzer",
|
||||
checked = externalNotificationInput.alertMessageBuzzer,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput =
|
||||
externalNotificationInput.copy { alertMessageBuzzer = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert message vibra",
|
||||
checked = externalNotificationInput.alertMessageVibra,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput =
|
||||
externalNotificationInput.copy { alertMessageVibra = it }
|
||||
})
|
||||
}
|
||||
|
||||
item { TextDividerPreference("Notifications on alert/bell receipt", enabled = enabled) }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert bell LED",
|
||||
checked = externalNotificationInput.alertBell,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { alertBell = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert bell buzzer",
|
||||
checked = externalNotificationInput.alertBellBuzzer,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput =
|
||||
externalNotificationInput.copy { alertBellBuzzer = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Alert bell vibra",
|
||||
checked = externalNotificationInput.alertBellVibra,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput =
|
||||
externalNotificationInput.copy { alertBellVibra = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Output LED (GPIO)",
|
||||
value = externalNotificationInput.output,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
externalNotificationInput = externalNotificationInput.copy { output = it }
|
||||
})
|
||||
}
|
||||
|
||||
if (externalNotificationInput.output != 0) item {
|
||||
SwitchPreference(title = "Output LED active high",
|
||||
checked = externalNotificationInput.active,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { active = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Output buzzer (GPIO)",
|
||||
value = externalNotificationInput.outputBuzzer,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
externalNotificationInput = externalNotificationInput.copy { outputBuzzer = it }
|
||||
})
|
||||
}
|
||||
|
||||
if (externalNotificationInput.outputBuzzer != 0) item {
|
||||
SwitchPreference(title = "Use PWM buzzer",
|
||||
checked = externalNotificationInput.usePwm,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { usePwm = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Output vibra (GPIO)",
|
||||
value = externalNotificationInput.outputVibra,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
externalNotificationInput = externalNotificationInput.copy { outputVibra = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Output duration (milliseconds)",
|
||||
value = externalNotificationInput.outputMs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
externalNotificationInput = externalNotificationInput.copy { outputMs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Nag timeout (seconds)",
|
||||
value = externalNotificationInput.nagTimeout,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
externalNotificationInput = externalNotificationInput.copy { nagTimeout = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Ringtone",
|
||||
value = ringtoneInput,
|
||||
maxSize = 230, // ringtone max_size:231
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { ringtoneInput = it }
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Use I2S as buzzer",
|
||||
checked = externalNotificationInput.useI2SAsBuzzer,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
externalNotificationInput = externalNotificationInput.copy { useI2SAsBuzzer = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.Channel
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.model.RegionInfo
|
||||
import com.geeksville.mesh.model.numChannels
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditListPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun LoRaConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
LoRaConfigItemList(
|
||||
loraConfig = state.radioConfig.lora,
|
||||
primarySettings = state.channelList.getOrNull(0) ?: return,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { loraInput ->
|
||||
val config = config { lora = loraInput }
|
||||
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 = "LoRa Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Use modem preset",
|
||||
checked = loraInput.usePreset,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
if (loraInput.usePreset) {
|
||||
item {
|
||||
DropDownPreference(title = "Modem preset",
|
||||
enabled = enabled && loraInput.usePreset,
|
||||
items = LoRaConfig.ModemPreset.entries
|
||||
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = loraInput.modemPreset,
|
||||
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
} else {
|
||||
item {
|
||||
EditTextPreference(title = "Bandwidth",
|
||||
value = loraInput.bandwidth,
|
||||
enabled = enabled && !loraInput.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { bandwidth = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Spread factor",
|
||||
value = loraInput.spreadFactor,
|
||||
enabled = enabled && !loraInput.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { spreadFactor = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Coding rate",
|
||||
value = loraInput.codingRate,
|
||||
enabled = enabled && !loraInput.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { codingRate = it } })
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Frequency offset (MHz)",
|
||||
value = loraInput.frequencyOffset,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Region (frequency plan)",
|
||||
enabled = enabled,
|
||||
items = RegionInfo.entries.map { it.regionCode to it.description },
|
||||
selectedItem = loraInput.region,
|
||||
onItemSelected = { loraInput = loraInput.copy { region = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Hop limit",
|
||||
value = loraInput.hopLimit,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { hopLimit = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "TX enabled",
|
||||
checked = loraInput.txEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "TX power (dBm)",
|
||||
value = loraInput.txPower,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { loraInput = loraInput.copy { txPower = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
EditTextPreference(title = "Frequency slot",
|
||||
value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
onValueChanged = {
|
||||
if (it <= loraInput.numChannels) { // total num of LoRa channels
|
||||
loraInput = loraInput.copy { channelNum = it }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Override Duty Cycle",
|
||||
checked = loraInput.overrideDutyCycle,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditListPreference(title = "Ignore incoming",
|
||||
list = loraInput.ignoreIncomingList,
|
||||
maxCount = 3, // ignore_incoming max_count:3
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValuesChanged = { list ->
|
||||
loraInput = loraInput.copy {
|
||||
ignoreIncoming.clear()
|
||||
ignoreIncoming.addAll(list.filter { it != 0 })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "SX126X RX boosted gain",
|
||||
checked = loraInput.sx126XRxBoostedGain,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { sx126XRxBoostedGain = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
EditTextPreference(title = "Override frequency (MHz)",
|
||||
value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } })
|
||||
}
|
||||
|
||||
if (hasPaFan) {
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = "PA fan disabled",
|
||||
checked = loraInput.paFanDisabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { paFanDisabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Ignore MQTT",
|
||||
checked = loraInput.ignoreMqtt,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "OK to MQTT",
|
||||
checked = loraInput.configOkToMqtt,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Divider
|
||||
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.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.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditPasswordPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PositionPrecisionPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
MQTTConfigItemList(
|
||||
mqttConfig = state.moduleConfig.mqtt,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { mqttInput ->
|
||||
val config = moduleConfig { mqtt = mqttInput }
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigItemList(
|
||||
mqttConfig: MQTTConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (MQTTConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "MQTT Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "MQTT enabled",
|
||||
checked = mqttInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { mqttInput = mqttInput.copy { this.enabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Address",
|
||||
value = mqttInput.address,
|
||||
maxSize = 63, // address max_size:64
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { mqttInput = mqttInput.copy { address = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Username",
|
||||
value = mqttInput.username,
|
||||
maxSize = 63, // username max_size:64
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { mqttInput = mqttInput.copy { username = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditPasswordPreference(title = "Password",
|
||||
value = mqttInput.password,
|
||||
maxSize = 63, // password max_size:64
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { mqttInput = mqttInput.copy { password = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Encryption enabled",
|
||||
checked = mqttInput.encryptionEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { mqttInput = mqttInput.copy { encryptionEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "JSON output enabled",
|
||||
checked = mqttInput.jsonEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { mqttInput = mqttInput.copy { jsonEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "TLS enabled",
|
||||
checked = mqttInput.tlsEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { mqttInput = mqttInput.copy { tlsEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Root topic",
|
||||
value = mqttInput.root,
|
||||
maxSize = 31, // root max_size:32
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { mqttInput = mqttInput.copy { root = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Proxy to client enabled",
|
||||
checked = mqttInput.proxyToClientEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { mqttInput = mqttInput.copy { proxyToClientEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
PositionPrecisionPreference(
|
||||
title = "Map reporting",
|
||||
enabled = enabled,
|
||||
value = mqttInput.mapReportSettings.positionPrecision,
|
||||
onValueChanged = {
|
||||
val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
|
||||
mqttInput = mqttInput.copy {
|
||||
mapReportingEnabled = settings.positionPrecision > 0
|
||||
mapReportSettings = settings
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Map reporting interval (seconds)",
|
||||
value = mqttInput.mapReportSettings.publishIntervalSecs,
|
||||
enabled = enabled && mqttInput.mapReportingEnabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it }
|
||||
mqttInput = mqttInput.copy { mapReportSettings = settings }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled && mqttInput != mqttConfig,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
mqttInput = mqttConfig
|
||||
},
|
||||
onSaveClicked = {
|
||||
focusManager.clearFocus()
|
||||
onSaveClicked(mqttInput)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun MQTTConfigPreview() {
|
||||
MQTTConfigItemList(
|
||||
mqttConfig = MQTTConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun NeighborInfoConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
NeighborInfoConfigItemList(
|
||||
neighborInfoConfig = state.moduleConfig.neighborInfo,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { neighborInfoInput ->
|
||||
val config = moduleConfig { neighborInfo = neighborInfoInput }
|
||||
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 = "Neighbor Info Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Neighbor Info enabled",
|
||||
checked = neighborInfoInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
neighborInfoInput = neighborInfoInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Update interval (seconds)",
|
||||
value = neighborInfoInput.updateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
neighborInfoInput = neighborInfoInput.copy { updateInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = "Transmit over LoRa",
|
||||
summary = stringResource(id = R.string.config_device_transmitOverLora_summary),
|
||||
checked = neighborInfoInput.transmitOverLora,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
neighborInfoInput = neighborInfoInput.copy { transmitOverLora = it }
|
||||
}
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Text
|
||||
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.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.NetworkConfig
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditIPv4Preference
|
||||
import com.geeksville.mesh.ui.components.EditPasswordPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SimpleAlertDialog
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
|
||||
@Composable
|
||||
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(),
|
||||
) {
|
||||
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) }
|
||||
|
||||
var showScanErrorDialog: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||
if (showScanErrorDialog) {
|
||||
ScanErrorDialog { showScanErrorDialog = false }
|
||||
}
|
||||
|
||||
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
if (result.contents != null) {
|
||||
val (ssid, psk) = extractWifiCredentials(result.contents)
|
||||
if (ssid != null && psk != null) {
|
||||
networkInput = networkInput.copy { wifiSsid = ssid; wifiPsk = psk }
|
||||
} else {
|
||||
showScanErrorDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun zxingScan() {
|
||||
val zxingScan = ScanOptions().apply {
|
||||
setCameraId(0)
|
||||
setPrompt("")
|
||||
setBeepEnabled(false)
|
||||
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||
}
|
||||
barcodeLauncher.launch(zxingScan)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "Network Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "WiFi enabled",
|
||||
checked = networkInput.wifiEnabled,
|
||||
enabled = enabled && hasWifi,
|
||||
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } })
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "SSID",
|
||||
value = networkInput.wifiSsid,
|
||||
maxSize = 32, // wifi_ssid max_size:33
|
||||
enabled = enabled && hasWifi,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
networkInput = networkInput.copy { wifiSsid = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditPasswordPreference(title = "PSK",
|
||||
value = networkInput.wifiPsk,
|
||||
maxSize = 64, // wifi_psk max_size:65
|
||||
enabled = enabled && hasWifi,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
Button(
|
||||
onClick = { zxingScan() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.height(48.dp),
|
||||
enabled = enabled && hasWifi,
|
||||
) {
|
||||
Text(text = stringResource(R.string.wifi_qr_code_scan))
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Ethernet enabled",
|
||||
checked = networkInput.ethEnabled,
|
||||
enabled = enabled && hasEthernet,
|
||||
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } })
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "NTP server",
|
||||
value = networkInput.ntpServer,
|
||||
maxSize = 32, // ntp_server max_size:33
|
||||
enabled = enabled,
|
||||
isError = networkInput.ntpServer.isEmpty(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
networkInput = networkInput.copy { ntpServer = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "rsyslog server",
|
||||
value = networkInput.rsyslogServer,
|
||||
maxSize = 32, // rsyslog_server max_size:33
|
||||
enabled = enabled,
|
||||
isError = false,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
networkInput = networkInput.copy { rsyslogServer = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "IPv4 mode",
|
||||
enabled = enabled,
|
||||
items = NetworkConfig.AddressMode.entries
|
||||
.filter { it != NetworkConfig.AddressMode.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = networkInput.addressMode,
|
||||
onItemSelected = { networkInput = networkInput.copy { addressMode = it } })
|
||||
Divider()
|
||||
}
|
||||
|
||||
item {
|
||||
EditIPv4Preference(title = "IP",
|
||||
value = networkInput.ipv4Config.ip,
|
||||
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = networkInput.ipv4Config.copy { ip = it }
|
||||
networkInput = networkInput.copy { ipv4Config = ipv4 }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditIPv4Preference(title = "Gateway",
|
||||
value = networkInput.ipv4Config.gateway,
|
||||
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = networkInput.ipv4Config.copy { gateway = it }
|
||||
networkInput = networkInput.copy { ipv4Config = ipv4 }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditIPv4Preference(title = "Subnet",
|
||||
value = networkInput.ipv4Config.subnet,
|
||||
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = networkInput.ipv4Config.copy { subnet = it }
|
||||
networkInput = networkInput.copy { ipv4Config = ipv4 }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditIPv4Preference(title = "DNS",
|
||||
value = networkInput.ipv4Config.dns,
|
||||
enabled = enabled && networkInput.addressMode == NetworkConfig.AddressMode.STATIC,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val ipv4 = networkInput.ipv4Config.copy { dns = it }
|
||||
networkInput = networkInput.copy { ipv4Config = ipv4 }
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.ResponseState
|
||||
|
||||
@Composable
|
||||
fun <T> PacketResponseStateDialog(
|
||||
state: ResponseState<T>,
|
||||
onDismiss: () -> Unit = {},
|
||||
onComplete: () -> Unit = {},
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
title = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (state is ResponseState.Loading) {
|
||||
val progress by animateFloatAsState(
|
||||
targetValue = state.completed.toFloat() / state.total.toFloat(),
|
||||
label = "progress",
|
||||
)
|
||||
Text("%.0f%%".format(progress * 100))
|
||||
LinearProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
)
|
||||
if (state.total == state.completed) onComplete()
|
||||
}
|
||||
if (state is ResponseState.Success) {
|
||||
Text(text = stringResource(id = R.string.delivery_confirmed))
|
||||
}
|
||||
if (state is ResponseState.Error) {
|
||||
Text(text = stringResource(id = R.string.error), minLines = 2)
|
||||
Text(text = state.error.asString())
|
||||
}
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
) { Text(stringResource(R.string.close)) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PacketResponseStateDialogPreview() {
|
||||
PacketResponseStateDialog(
|
||||
state = ResponseState.Loading(
|
||||
total = 17,
|
||||
completed = 5,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun PaxcounterConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
PaxcounterConfigItemList(
|
||||
paxcounterConfig = state.moduleConfig.paxcounter,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { paxcounterConfigInput ->
|
||||
val config = moduleConfig { paxcounter = paxcounterConfigInput }
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 = "Paxcounter Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Paxcounter enabled",
|
||||
checked = paxcounterInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
paxcounterInput = paxcounterInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Update interval (seconds)",
|
||||
value = paxcounterInput.paxcounterUpdateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
paxcounterInput = paxcounterInput.copy { paxcounterUpdateInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "WiFi RSSI threshold (defaults to -80)",
|
||||
value = paxcounterInput.wifiThreshold,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
paxcounterInput = paxcounterInput.copy { wifiThreshold = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "BLE RSSI threshold (defaults to -80)",
|
||||
value = paxcounterInput.bleThreshold,
|
||||
enabled = enabled,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PaxcounterConfigPreview() {
|
||||
PaxcounterConfigItemList(
|
||||
paxcounterConfig = ModuleConfigProtos.ModuleConfig.PaxcounterConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.ConfigProtos.Config.PositionConfig
|
||||
import com.geeksville.mesh.Position
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.BitwisePreference
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun PositionConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
val node by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val currentPosition = Position(
|
||||
latitude = node?.latitude ?: 0.0,
|
||||
longitude = node?.longitude ?: 0.0,
|
||||
altitude = node?.position?.altitude ?: 0,
|
||||
time = 1, // ignore time for fixed_position
|
||||
)
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
PositionConfigItemList(
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun PositionConfigItemList(
|
||||
location: Position,
|
||||
positionConfig: PositionConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (position: Position, config: PositionConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var locationInput by rememberSaveable { mutableStateOf(location) }
|
||||
var positionInput by rememberSaveable { mutableStateOf(positionConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "Position Config") }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Position broadcast interval (seconds)",
|
||||
value = positionInput.positionBroadcastSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
positionInput = positionInput.copy { positionBroadcastSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Smart position enabled",
|
||||
checked = positionInput.positionBroadcastSmartEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
positionInput = positionInput.copy { positionBroadcastSmartEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
if (positionInput.positionBroadcastSmartEnabled) {
|
||||
item {
|
||||
EditTextPreference(title = "Smart broadcast minimum distance (meters)",
|
||||
value = positionInput.broadcastSmartMinimumDistance,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
positionInput = positionInput.copy { broadcastSmartMinimumDistance = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Smart broadcast minimum interval (seconds)",
|
||||
value = positionInput.broadcastSmartMinimumIntervalSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Use fixed position",
|
||||
checked = positionInput.fixedPosition,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
if (positionInput.fixedPosition) {
|
||||
item {
|
||||
EditTextPreference(title = "Latitude",
|
||||
value = locationInput.latitude,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { value ->
|
||||
if (value >= -90 && value <= 90.0) {
|
||||
locationInput = locationInput.copy(latitude = value)
|
||||
}
|
||||
})
|
||||
}
|
||||
item {
|
||||
EditTextPreference(title = "Longitude",
|
||||
value = locationInput.longitude,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { value ->
|
||||
if (value >= -180 && value <= 180.0) {
|
||||
locationInput = locationInput.copy(longitude = value)
|
||||
}
|
||||
})
|
||||
}
|
||||
item {
|
||||
EditTextPreference(title = "Altitude (meters)",
|
||||
value = locationInput.altitude,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { value ->
|
||||
locationInput = locationInput.copy(altitude = value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "GPS mode",
|
||||
enabled = enabled,
|
||||
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 } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "GPS update interval (seconds)",
|
||||
value = positionInput.gpsUpdateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
BitwisePreference(title = "Position flags",
|
||||
value = positionInput.positionFlags,
|
||||
enabled = enabled,
|
||||
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 } }
|
||||
)
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Redefine GPS_RX_PIN",
|
||||
value = positionInput.rxGpio,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { positionInput = positionInput.copy { rxGpio = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Redefine GPS_TX_PIN",
|
||||
value = positionInput.txGpio,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { positionInput = positionInput.copy { txGpio = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Redefine PIN_GPS_EN",
|
||||
value = positionInput.gpsEnGpio,
|
||||
enabled = enabled,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PositionConfigPreview() {
|
||||
PositionConfigItemList(
|
||||
location = Position(0.0, 0.0, 0),
|
||||
positionConfig = PositionConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { _, _ -> },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.PowerConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun PowerConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
PowerConfigItemList(
|
||||
powerConfig = state.radioConfig.power,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { powerInput ->
|
||||
val config = config { power = powerInput }
|
||||
viewModel.setConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PowerConfigItemList(
|
||||
powerConfig: PowerConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (PowerConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var powerInput by rememberSaveable { mutableStateOf(powerConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "Power Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Enable power saving mode",
|
||||
checked = powerInput.isPowerSaving,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Shutdown on battery delay (seconds)",
|
||||
value = powerInput.onBatteryShutdownAfterSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "ADC multiplier override ratio",
|
||||
value = powerInput.adcMultiplierOverride,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Wait for Bluetooth duration (seconds)",
|
||||
value = powerInput.waitBluetoothSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { powerInput = powerInput.copy { waitBluetoothSecs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Super deep sleep duration (seconds)",
|
||||
value = powerInput.sdsSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { powerInput = powerInput.copy { sdsSecs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Light sleep duration (seconds)",
|
||||
value = powerInput.lsSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { powerInput = powerInput.copy { lsSecs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Minimum wake time (seconds)",
|
||||
value = powerInput.minWakeSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { powerInput = powerInput.copy { minWakeSecs = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Battery INA_2XX I2C address",
|
||||
value = powerInput.deviceBatteryInaAddress,
|
||||
enabled = enabled,
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PowerConfigPreview() {
|
||||
PowerConfigItemList(
|
||||
powerConfig = PowerConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onSaveClicked = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RangeTestConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun RangeTestConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
RangeTestConfigItemList(
|
||||
rangeTestConfig = state.moduleConfig.rangeTest,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { rangeTestInput ->
|
||||
val config = moduleConfig { rangeTest = rangeTestInput }
|
||||
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 = "Range Test Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Range test enabled",
|
||||
checked = rangeTestInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { rangeTestInput = rangeTestInput.copy { this.enabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Sender message interval (seconds)",
|
||||
value = rangeTestInput.sender,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { rangeTestInput = rangeTestInput.copy { sender = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Save .CSV in storage (ESP32 only)",
|
||||
checked = rangeTestInput.save,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { rangeTestInput = rangeTestInput.copy { save = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.RemoteHardwareConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditListPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun RemoteHardwareConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
RemoteHardwareConfigItemList(
|
||||
remoteHardwareConfig = state.moduleConfig.remoteHardware,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { remoteHardwareInput ->
|
||||
val config = moduleConfig { remoteHardware = remoteHardwareInput }
|
||||
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 = "Remote Hardware Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Remote Hardware enabled",
|
||||
checked = remoteHardwareInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
remoteHardwareInput = remoteHardwareInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Allow undefined pin access",
|
||||
checked = remoteHardwareInput.allowUndefinedPinAccess,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
remoteHardwareInput = remoteHardwareInput.copy { allowUndefinedPinAccess = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditListPreference(title = "Available pins",
|
||||
list = remoteHardwareInput.availablePinsList,
|
||||
maxCount = 4, // available_pins max_count:4
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValuesChanged = { list ->
|
||||
remoteHardwareInput = remoteHardwareInput.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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.components.EditBase64Preference
|
||||
import com.geeksville.mesh.ui.components.EditListPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun SecurityConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
SecurityConfigItemList(
|
||||
securityConfig = state.radioConfig.security,
|
||||
enabled = state.connected,
|
||||
onConfirm = { securityInput ->
|
||||
val config = config { security = securityInput }
|
||||
viewModel.setConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SecurityConfigItemList(
|
||||
securityConfig: SecurityConfig,
|
||||
enabled: Boolean,
|
||||
onConfirm: (config: SecurityConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var securityInput by rememberSaveable { mutableStateOf(securityConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "Security Config") }
|
||||
|
||||
item {
|
||||
EditBase64Preference(
|
||||
title = "Public Key",
|
||||
value = securityInput.publicKey,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
if (it.size() == 32) {
|
||||
securityInput = securityInput.copy { publicKey = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EditBase64Preference(
|
||||
title = "Private Key",
|
||||
value = securityInput.privateKey,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChange = {
|
||||
if (it.size() == 32) {
|
||||
securityInput = securityInput.copy { privateKey = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EditListPreference(
|
||||
title = "Admin Key",
|
||||
list = securityInput.adminKeyList,
|
||||
maxCount = 3,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValuesChanged = {
|
||||
securityInput = securityInput.copy {
|
||||
adminKey.clear()
|
||||
adminKey.addAll(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Managed Mode",
|
||||
checked = securityInput.isManaged,
|
||||
enabled = enabled && securityInput.adminKeyCount > 0,
|
||||
onCheckedChange = {
|
||||
securityInput = securityInput.copy { isManaged = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Serial console",
|
||||
checked = securityInput.serialEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Debug log API enabled",
|
||||
checked = securityInput.debugLogApiEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
securityInput = securityInput.copy { debugLogApiEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Legacy Admin channel",
|
||||
checked = securityInput.adminChannelEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
securityInput = securityInput.copy { adminChannelEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled && securityInput != securityConfig,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
securityInput = securityConfig
|
||||
},
|
||||
onSaveClicked = {
|
||||
focusManager.clearFocus()
|
||||
onConfirm(securityInput)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SecurityConfigPreview() {
|
||||
SecurityConfigItemList(
|
||||
securityConfig = SecurityConfig.getDefaultInstance(),
|
||||
enabled = true,
|
||||
onConfirm = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.SerialConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun SerialConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
SerialConfigItemList(
|
||||
serialConfig = state.moduleConfig.serial,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { serialInput ->
|
||||
val config = moduleConfig { serial = serialInput }
|
||||
viewModel.setModuleConfig(config)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@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 = "Serial Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Serial enabled",
|
||||
checked = serialInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { serialInput = serialInput.copy { this.enabled = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Echo enabled",
|
||||
checked = serialInput.echo,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { serialInput = serialInput.copy { echo = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "RX",
|
||||
value = serialInput.rxd,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { serialInput = serialInput.copy { rxd = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "TX",
|
||||
value = serialInput.txd,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { serialInput = serialInput.copy { txd = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Serial baud rate",
|
||||
enabled = enabled,
|
||||
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 } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Timeout",
|
||||
value = serialInput.timeout,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { serialInput = serialInput.copy { timeout = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
DropDownPreference(title = "Serial mode",
|
||||
enabled = enabled,
|
||||
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 } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Override console serial port",
|
||||
checked = serialInput.overrideConsoleSerialPort,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
serialInput = serialInput.copy { overrideConsoleSerialPort = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.StoreForwardConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun StoreForwardConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
StoreForwardConfigItemList(
|
||||
storeForwardConfig = state.moduleConfig.storeForward,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { storeForwardInput ->
|
||||
val config = moduleConfig { storeForward = storeForwardInput }
|
||||
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 = "Store & Forward Config") }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Store & Forward enabled",
|
||||
checked = storeForwardInput.enabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
storeForwardInput = storeForwardInput.copy { this.enabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Heartbeat",
|
||||
checked = storeForwardInput.heartbeat,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { storeForwardInput = storeForwardInput.copy { heartbeat = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Number of records",
|
||||
value = storeForwardInput.records,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { storeForwardInput = storeForwardInput.copy { records = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "History return max",
|
||||
value = storeForwardInput.historyReturnMax,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
storeForwardInput = storeForwardInput.copy { historyReturnMax = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "History return window",
|
||||
value = storeForwardInput.historyReturnWindow,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
storeForwardInput = storeForwardInput.copy { historyReturnWindow = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = "Server",
|
||||
checked = storeForwardInput.isServer,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { storeForwardInput = storeForwardInput.copy { isServer = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.Divider
|
||||
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.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.TelemetryConfig
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun TelemetryConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
TelemetryConfigItemList(
|
||||
telemetryConfig = state.moduleConfig.telemetry,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { telemetryInput ->
|
||||
val config = moduleConfig { telemetry = telemetryInput }
|
||||
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 = "Telemetry Config") }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Device metrics update interval (seconds)",
|
||||
value = telemetryInput.deviceUpdateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
telemetryInput = telemetryInput.copy { deviceUpdateInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Environment metrics update interval (seconds)",
|
||||
value = telemetryInput.environmentUpdateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
telemetryInput = telemetryInput.copy { environmentUpdateInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Environment metrics module enabled",
|
||||
checked = telemetryInput.environmentMeasurementEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { environmentMeasurementEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Environment metrics on-screen enabled",
|
||||
checked = telemetryInput.environmentScreenEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { environmentScreenEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Environment metrics use Fahrenheit",
|
||||
checked = telemetryInput.environmentDisplayFahrenheit,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { environmentDisplayFahrenheit = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Air quality metrics module enabled",
|
||||
checked = telemetryInput.airQualityEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { airQualityEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Air quality metrics update interval (seconds)",
|
||||
value = telemetryInput.airQualityInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
telemetryInput = telemetryInput.copy { airQualityInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Power metrics module enabled",
|
||||
checked = telemetryInput.powerMeasurementEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { powerMeasurementEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Power metrics update interval (seconds)",
|
||||
value = telemetryInput.powerUpdateInterval,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
telemetryInput = telemetryInput.copy { powerUpdateInterval = it }
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Power metrics on-screen enabled",
|
||||
checked = telemetryInput.powerScreenEnabled,
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
telemetryInput = telemetryInput.copy { powerScreenEnabled = it }
|
||||
})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* 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.components.config
|
||||
|
||||
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.material.Divider
|
||||
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.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
import com.geeksville.mesh.ui.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.components.PreferenceFooter
|
||||
import com.geeksville.mesh.ui.components.RegularPreference
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.geeksville.mesh.user
|
||||
|
||||
@Composable
|
||||
fun UserConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
state = state.responseState,
|
||||
onDismiss = viewModel::clearPacketResponse,
|
||||
)
|
||||
}
|
||||
|
||||
UserConfigItemList(
|
||||
userConfig = state.userConfig,
|
||||
enabled = true,
|
||||
onSaveClicked = viewModel::setOwner,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserConfigItemList(
|
||||
userConfig: MeshProtos.User,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (MeshProtos.User) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var userInput by rememberSaveable { mutableStateOf(userConfig) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item { PreferenceCategory(text = "User Config") }
|
||||
|
||||
item {
|
||||
RegularPreference(title = "Node ID",
|
||||
subtitle = userInput.id,
|
||||
onClick = {})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Long name",
|
||||
value = userInput.longName,
|
||||
maxSize = 39, // long_name max_size:40
|
||||
enabled = enabled,
|
||||
isError = userInput.longName.isEmpty(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
userInput = userInput.copy { longName = it }
|
||||
if (getInitials(it).toByteArray().size <= 4) { // short_name max_size:5
|
||||
userInput = userInput.copy { shortName = getInitials(it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(title = "Short name",
|
||||
value = userInput.shortName,
|
||||
maxSize = 4, // short_name max_size:5
|
||||
enabled = enabled,
|
||||
isError = userInput.shortName.isEmpty(),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Text, imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { userInput = userInput.copy { shortName = it } })
|
||||
}
|
||||
|
||||
item {
|
||||
RegularPreference(title = "Hardware model",
|
||||
subtitle = userInput.hwModel.name,
|
||||
onClick = {})
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(title = "Licensed amateur radio",
|
||||
checked = userInput.isLicensed,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { userInput = userInput.copy { isLicensed = it } })
|
||||
}
|
||||
item { Divider() }
|
||||
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled && userInput != userConfig,
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue