add user & device config settings (#520)

* add MeshUser & LocalConfig prefs
This commit is contained in:
Andre K 2022-11-08 23:11:18 -03:00 committed by GitHub
parent 17dc4da191
commit 4bcd408dce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1383 additions and 200 deletions

View file

@ -18,11 +18,12 @@ data class MeshUser(
val id: String,
val longName: String,
val shortName: String,
val hwModel: MeshProtos.HardwareModel
val hwModel: MeshProtos.HardwareModel,
val isLicensed: Boolean,
) : Parcelable {
override fun toString(): String {
return "MeshUser(id=${id.anonymize}, longName=${longName.anonymize}, shortName=${shortName.anonymize}, hwModel=${hwModelString})"
return "MeshUser(id=${id.anonymize}, longName=${longName.anonymize}, shortName=${shortName.anonymize}, hwModel=${hwModelString}, isLicensed=${isLicensed})"
}
/** a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot

View file

@ -6,25 +6,21 @@ import com.geeksville.mesh.R
enum class ChannelOption(
val modemPreset: ModemPreset,
val configRes: Int,
val minBroadcastPeriodSecs: Int
) {
SHORT_FAST(ModemPreset.SHORT_FAST, R.string.modem_config_short, 30),
SHORT_SLOW(ModemPreset.SHORT_SLOW, R.string.modem_config_slow_short, 30),
MEDIUM_FAST(ModemPreset.MEDIUM_FAST, R.string.modem_config_medium, 60),
MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, R.string.modem_config_slow_medium, 60),
LONG_FAST(ModemPreset.LONG_FAST, R.string.modem_config_long, 60),
LONG_SLOW(ModemPreset.LONG_SLOW, R.string.modem_config_slow_long, 240),
VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, R.string.modem_config_very_long, 375);
SHORT_FAST(ModemPreset.SHORT_FAST, R.string.modem_config_short),
SHORT_SLOW(ModemPreset.SHORT_SLOW, R.string.modem_config_slow_short),
MEDIUM_FAST(ModemPreset.MEDIUM_FAST, R.string.modem_config_medium),
MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, R.string.modem_config_slow_medium),
LONG_FAST(ModemPreset.LONG_FAST, R.string.modem_config_long),
LONG_SLOW(ModemPreset.LONG_SLOW, R.string.modem_config_slow_long),
VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, R.string.modem_config_very_long);
companion object {
fun fromConfig(modemPreset: ModemPreset?): ChannelOption? {
for (option in values()) {
if (option.modemPreset == modemPreset)
return option
if (option.modemPreset == modemPreset) return option
}
return null
}
val defaultMinBroadcastPeriod = VERY_LONG_SLOW.minBroadcastPeriodSecs
}
}

View file

@ -22,7 +22,8 @@ class NodeDB(private val ui: UIViewModel) {
"+16508765308".format(8),
"Kevin MesterNoLoc",
"KLO",
MeshProtos.HardwareModel.ANDROID_SIM
MeshProtos.HardwareModel.ANDROID_SIM,
false
),
null
)
@ -34,7 +35,8 @@ class NodeDB(private val ui: UIViewModel) {
"+165087653%02d".format(9 + index),
"Kevin Mester$index",
"KM$index",
MeshProtos.HardwareModel.ANDROID_SIM
MeshProtos.HardwareModel.ANDROID_SIM,
false
),
it
)

View file

@ -34,7 +34,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.osmdroid.bonuspack.kml.KmlDocument
@ -110,21 +112,17 @@ class UIViewModel @Inject constructor(
_packets.value = packets
}
}
viewModelScope.launch {
localConfigRepository.localConfigFlow.collect { config ->
_localConfig.value = config
}
}
localConfigRepository.localConfigFlow.onEach { config ->
_localConfig.value = config
}.launchIn(viewModelScope)
viewModelScope.launch {
quickChatActionRepository.getAllActions().collect { actions ->
_quickChatActions.value = actions
}
}
viewModelScope.launch {
channelSetRepository.channelSetFlow.collect { channelSet ->
_channels.value = ChannelSet(channelSet)
}
}
channelSetRepository.channelSetFlow.onEach { channelSet ->
_channels.value = ChannelSet(channelSet)
}.launchIn(viewModelScope)
debug("ViewModel created")
}
@ -240,7 +238,7 @@ class UIViewModel @Inject constructor(
val isRouter: Boolean = config.device.role == Config.DeviceConfig.Role.ROUTER
// We consider hasWifi = ESP32
fun isESP32() = myNodeInfo.value?.hasWifi == true
fun hasWifi() = myNodeInfo.value?.hasWifi == true
/// hardware info about our local device (can be null)
private val _myNodeInfo = MutableLiveData<MyNodeInfo?>()
@ -364,14 +362,14 @@ class UIViewModel @Inject constructor(
}
// clean up all this nasty owner state management FIXME
fun setOwner(s: String? = null) {
fun setOwner(longName: String? = null, shortName: String? = null, isLicensed: Boolean? = null) {
if (s != null) {
_ownerName.value = s
if (longName != null) {
_ownerName.value = longName
// note: we allow an empty userstring to be written to prefs
preferences.edit {
putString("owner", s)
putString("owner", longName)
}
}
@ -381,7 +379,8 @@ class UIViewModel @Inject constructor(
meshService?.setOwner(
null,
_ownerName.value,
getInitials(_ownerName.value!!)
shortName ?: getInitials(_ownerName.value!!),
isLicensed ?: false
) // Note: we use ?. here because we might be running in the emulator
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")

View file

@ -732,7 +732,8 @@ class MeshService : Service(), Logging {
p.id.ifEmpty { oldId }, // If the new update doesn't contain an ID keep our old value
p.longName,
p.shortName,
p.hwModel
p.hwModel,
p.isLicensed
)
}
}
@ -1155,7 +1156,8 @@ class MeshService : Service(), Logging {
info.user.id,
info.user.longName,
info.user.shortName,
info.user.hwModel
info.user.hwModel,
info.user.isLicensed
)
if (info.hasPosition()) {
@ -1454,7 +1456,7 @@ class MeshService : Service(), Logging {
/**
* Set our owner with either the new or old API
*/
fun setOwner(myId: String?, longName: String, shortName: String) {
fun setOwner(myId: String?, longName: String, shortName: String, isLicensed: Boolean) {
val myNode = myNodeInfo
if (myNode != null) {
@ -1468,6 +1470,7 @@ class MeshService : Service(), Logging {
it.id = myId
it.longName = longName
it.shortName = shortName
it.isLicensed = isLicensed
}.build()
// Also update our own map for our nodenum, by handling the packet just like packets from other users
@ -1608,9 +1611,9 @@ class MeshService : Service(), Logging {
override fun getMyId() = toRemoteExceptions { myNodeID }
override fun setOwner(myId: String?, longName: String, shortName: String) =
override fun setOwner(myId: String?, longName: String, shortName: String, isLicensed: Boolean) =
toRemoteExceptions {
this@MeshService.setOwner(myId, longName, shortName)
this@MeshService.setOwner(myId, longName, shortName, isLicensed)
}
override fun send(p: DataPacket) {

View file

@ -4,25 +4,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hideKeyboard
import com.geeksville.mesh.R
import com.geeksville.mesh.copy
import com.geeksville.mesh.databinding.AdvancedSettingsBinding
import com.geeksville.mesh.model.ChannelOption
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.util.exceptionToSnackbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
private val MAX_INT_DEVICE = 0xFFFFFFFF
private var _binding: AdvancedSettingsBinding? = null
private val binding get() = _binding!!
@ -34,79 +26,18 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
savedInstanceState: Bundle?
): View {
_binding = AdvancedSettingsBinding.inflate(inflater, container, false)
.apply {
deviceConfig.apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MdcTheme {
PreferenceScreen(model)
}
}
}
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.localConfig.asLiveData().observe(viewLifecycleOwner) {
binding.positionBroadcastPeriodEditText.setText(model.config.position.positionBroadcastSecs.toString())
binding.lsSleepEditText.setText(model.config.power.lsSecs.toString())
binding.positionBroadcastPeriodView.isEnabled = model.config.position.gpsEnabled
binding.positionBroadcastSwitch.isChecked = model.config.position.gpsEnabled
binding.lsSleepView.isEnabled = model.config.power.isPowerSaving && model.isESP32()
binding.lsSleepSwitch.isChecked = model.config.power.isPowerSaving && model.isESP32()
}
model.connectionState.observe(viewLifecycleOwner) { connectionState ->
val connected = connectionState == MeshService.ConnectionState.CONNECTED
binding.positionBroadcastPeriodView.isEnabled = connected && model.config.position.gpsEnabled
binding.lsSleepView.isEnabled = connected && model.config.power.isPowerSaving
binding.positionBroadcastSwitch.isEnabled = connected
binding.lsSleepSwitch.isEnabled = connected && model.isESP32()
}
binding.positionBroadcastPeriodEditText.on(EditorInfo.IME_ACTION_DONE) {
val textEdit = binding.positionBroadcastPeriodEditText
val n = textEdit.text.toString().toIntOrNull()
val minBroadcastPeriodSecs =
ChannelOption.fromConfig(model.config.lora.modemPreset)?.minBroadcastPeriodSecs
?: ChannelOption.defaultMinBroadcastPeriod
if (n != null && n < MAX_INT_DEVICE && (n == 0 || n >= minBroadcastPeriodSecs)) {
exceptionToSnackbar(requireView()) {
model.updatePositionConfig { it.copy { positionBroadcastSecs = n } }
}
} else {
// restore the value in the edit field
textEdit.setText(model.config.position.positionBroadcastSecs.toString())
val errorText =
if (n == null || n < 0 || n >= MAX_INT_DEVICE)
"Bad value: ${textEdit.text.toString()}"
else
getString(R.string.broadcast_period_too_small).format(minBroadcastPeriodSecs)
Snackbar.make(requireView(), errorText, Snackbar.LENGTH_LONG).show()
}
requireActivity().hideKeyboard()
}
binding.positionBroadcastSwitch.setOnCheckedChangeListener { btn, isChecked ->
if (btn.isPressed) {
model.updatePositionConfig { it.copy { gpsEnabled = isChecked } }
debug("User changed locationShare to $isChecked")
}
}
binding.lsSleepEditText.on(EditorInfo.IME_ACTION_DONE) {
val str = binding.lsSleepEditText.text.toString()
val n = str.toIntOrNull()
if (n != null && n < MAX_INT_DEVICE && n >= 0) {
exceptionToSnackbar(requireView()) {
model.updatePowerConfig { it.copy { lsSecs = n } }
}
} else {
Snackbar.make(requireView(), "Bad value: $str", Snackbar.LENGTH_LONG).show()
}
requireActivity().hideKeyboard()
}
binding.lsSleepSwitch.setOnCheckedChangeListener { btn, isChecked ->
if (btn.isPressed) {
model.updatePowerConfig { it.copy { isPowerSaving = isChecked } }
debug("User changed isPowerSaving to $isChecked")
}
}
}
}
}

View file

@ -21,7 +21,6 @@ import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hideKeyboard
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigKt.loRaConfig
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.android.getCameraPermissions

View file

@ -0,0 +1,807 @@
package com.geeksville.mesh.ui
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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 com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
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.RegularPreference
import com.geeksville.mesh.ui.components.SwitchPreference
private fun Int.uintToString(): String = this.toUInt().toString()
private fun String.stringToIntOrNull(): Int? = this.toUIntOrNull()?.toInt()
@Composable
fun PreferenceItemList(viewModel: UIViewModel) {
val focusManager = LocalFocusManager.current
val hasWifi = viewModel.hasWifi()
val connectionState = viewModel.connectionState.observeAsState()
val connected = connectionState.value == MeshService.ConnectionState.CONNECTED
val localConfig by viewModel.localConfig.collectAsState()
val user = viewModel.nodeDB.ourNodeInfo?.user
// Temporary [ConfigProtos.Config] state holders
var userInput by remember { mutableStateOf(user) }
var deviceInput by remember { mutableStateOf(localConfig.device) }
var positionInput by remember { mutableStateOf(localConfig.position) }
var powerInput by remember { mutableStateOf(localConfig.power) }
var networkInput by remember { mutableStateOf(localConfig.network) }
var displayInput by remember { mutableStateOf(localConfig.display) }
var loraInput by remember { mutableStateOf(localConfig.lora) }
var bluetoothInput by remember { mutableStateOf(localConfig.bluetooth) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item { PreferenceCategory(text = "User Config") }
item {
RegularPreference(
title = "Node ID",
subtitle = userInput?.id ?: stringResource(id = R.string.unknown),
onClick = {})
}
item { Divider() }
item {
EditTextPreference(title = "Long name",
value = userInput?.longName ?: stringResource(id = R.string.unknown_username),
enabled = connected && userInput?.longName != null,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
userInput?.let { userInput = it.copy(longName = value) }
})
}
item {
EditTextPreference(title = "Short name",
value = userInput?.shortName ?: stringResource(id = R.string.unknown),
enabled = connected && userInput?.shortName != null,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
userInput?.let { userInput = it.copy(shortName = value) }
})
}
item {
RegularPreference(
title = "Hardware model",
subtitle = userInput?.hwModel?.name ?: stringResource(id = R.string.unknown),
onClick = {})
}
item { Divider() }
item {
SwitchPreference(title = "Licensed amateur radio",
checked = userInput?.isLicensed ?: false,
enabled = connected && userInput?.isLicensed != null,
onCheckedChange = { value ->
userInput?.let { userInput = it.copy(isLicensed = value) }
})
}
item { Divider() }
item {
PreferenceFooter(
enabled = userInput != user,
onCancelClicked = { userInput = user },
onSaveClicked = {
focusManager.clearFocus()
userInput?.let { viewModel.setOwner(it.longName, it.shortName, it.isLicensed) }
})
}
item { PreferenceCategory(text = "Device Config") }
item {
DropDownPreference(title = "Role",
enabled = connected,
items = ConfigProtos.Config.DeviceConfig.Role.values()
.filter { it != ConfigProtos.Config.DeviceConfig.Role.UNRECOGNIZED }
.map { it to it.name },
selectedItem = deviceInput.role,
onItemSelected = { deviceInput = deviceInput.copy { role = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Serial output enabled",
checked = deviceInput.serialEnabled,
enabled = connected,
onCheckedChange = { deviceInput = deviceInput.copy { serialEnabled = it } })
}
item { Divider() }
item {
SwitchPreference(title = "Debug log enabled",
checked = deviceInput.debugLogEnabled,
enabled = connected,
onCheckedChange = { deviceInput = deviceInput.copy { debugLogEnabled = it } })
}
item { Divider() }
item {
PreferenceFooter(
enabled = deviceInput != localConfig.device,
onCancelClicked = { deviceInput = localConfig.device },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateDeviceConfig { deviceInput }
})
}
item { PreferenceCategory(text = "Position Config") }
item {
EditTextPreference(title = "Position broadcast interval",
value = positionInput.positionBroadcastSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { positionInput = positionInput.copy { positionBroadcastSecs = it } }
})
}
item {
SwitchPreference(title = "Smart position enabled",
checked = positionInput.positionBroadcastSmartEnabled,
enabled = connected,
onCheckedChange = {
positionInput = positionInput.copy { positionBroadcastSmartEnabled = it }
})
}
item { Divider() }
item {
SwitchPreference(title = "Use fixed position",
checked = positionInput.fixedPosition,
enabled = connected,
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } })
}
item { Divider() }
item {
SwitchPreference(title = "GPS enabled",
checked = positionInput.gpsEnabled,
enabled = connected,
onCheckedChange = { positionInput = positionInput.copy { gpsEnabled = it } })
}
item { Divider() }
item {
EditTextPreference(title = "GPS update interval",
value = positionInput.gpsUpdateInterval.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { positionInput = positionInput.copy { gpsUpdateInterval = it } }
})
}
item {
EditTextPreference(title = "Fix attempt duration",
value = positionInput.gpsAttemptTime.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { positionInput = positionInput.copy { gpsAttemptTime = it } }
})
}
// TODO add positionFlags
item {
PreferenceFooter(
enabled = positionInput != localConfig.position,
onCancelClicked = { positionInput = localConfig.position },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updatePositionConfig { positionInput }
})
}
item { PreferenceCategory(text = "Power Config") }
item {
SwitchPreference(title = "Enable power saving mode",
checked = powerInput.isPowerSaving,
enabled = connected && hasWifi, // We consider hasWifi = ESP32
onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } })
}
item { Divider() }
item {
EditTextPreference(
title = "Shutdown on battery delay",
value = powerInput.onBatteryShutdownAfterSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } }
})
}
item {
EditTextPreference(
title = "ADC multiplier override ratio",
value = powerInput.adcMultiplierOverride.toString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.toFloatOrNull()
?.let { powerInput = powerInput.copy { adcMultiplierOverride = it } }
})
}
item {
EditTextPreference(
title = "Wait for Bluetooth duration",
value = powerInput.waitBluetoothSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { waitBluetoothSecs = it } }
})
}
item {
EditTextPreference(
title = "Mesh SDS timeout",
value = powerInput.meshSdsTimeoutSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { meshSdsTimeoutSecs = it } }
})
}
item {
EditTextPreference(
title = "Super deep sleep duration",
value = powerInput.sdsSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { sdsSecs = it } }
})
}
item {
EditTextPreference(
title = "Light sleep duration",
value = powerInput.lsSecs.uintToString(),
enabled = connected && hasWifi, // we consider hasWifi = ESP32
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { lsSecs = it } }
})
}
item {
EditTextPreference(
title = "Minimum wake time",
value = powerInput.minWakeSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { powerInput = powerInput.copy { minWakeSecs = it } }
})
}
item {
PreferenceFooter(
enabled = powerInput != localConfig.power,
onCancelClicked = { powerInput = localConfig.power },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updatePowerConfig { powerInput }
})
}
item { PreferenceCategory(text = "Network Config") }
item {
SwitchPreference(
title = "WiFi enabled",
checked = networkInput.wifiEnabled,
enabled = connected && hasWifi,
onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } })
}
item { Divider() }
item {
EditTextPreference(
title = "SSID",
value = networkInput.wifiSsid.toString(),
enabled = connected && hasWifi,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } })
}
item {
EditTextPreference(
title = "PSK",
value = networkInput.wifiPsk .toString(),
enabled = connected && hasWifi,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Password, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } })
}
item {
EditTextPreference(
title = "NTP server",
value = networkInput.ntpServer.toString(),
enabled = connected && hasWifi,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Uri, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { networkInput = networkInput.copy { ntpServer = it } })
}
item {
SwitchPreference(
title = "Ethernet enabled",
checked = networkInput.ethEnabled,
enabled = connected,
onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Ethernet mode",
enabled = connected,
items = ConfigProtos.Config.NetworkConfig.EthMode.values()
.filter { it != ConfigProtos.Config.NetworkConfig.EthMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = networkInput.ethMode,
onItemSelected = { networkInput = networkInput.copy { ethMode = it } })
}
item { Divider() }
item { PreferenceCategory(text = "IPv4 Config") }
item {
EditTextPreference(
title = "IP",
value = networkInput.ipv4Config.ip.toString(),
enabled = connected && networkInput.ethMode == ConfigProtos.Config.NetworkConfig.EthMode.STATIC,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()?.let {
networkInput = networkInput.copy { ipv4Config.copy { ip = it } }
}
})
}
item {
EditTextPreference(
title = "Gateway",
value = networkInput.ipv4Config.gateway.toString(),
enabled = connected && networkInput.ethMode == ConfigProtos.Config.NetworkConfig.EthMode.STATIC,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()?.let {
networkInput = networkInput.copy { ipv4Config.copy { gateway = it } }
}
})
}
item {
EditTextPreference(
title = "Subnet",
value = networkInput.ipv4Config.subnet.toString(),
enabled = connected && networkInput.ethMode == ConfigProtos.Config.NetworkConfig.EthMode.STATIC,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()?.let {
networkInput = networkInput.copy { ipv4Config.copy { subnet = it } }
}
})
}
item {
EditTextPreference(
title = "DNS",
value = networkInput.ipv4Config.dns.toString(),
enabled = connected && networkInput.ethMode == ConfigProtos.Config.NetworkConfig.EthMode.STATIC,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text, imeAction = ImeAction.Send
),
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()?.let {
networkInput = networkInput.copy { ipv4Config.copy { dns = it } }
}
})
}
item {
PreferenceFooter(
enabled = networkInput != localConfig.network,
onCancelClicked = { networkInput = localConfig.network },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateNetworkConfig { networkInput }
})
}
item { PreferenceCategory(text = "Display Config") }
item {
EditTextPreference(
title = "Screen timeout",
value = displayInput.screenOnSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { displayInput = displayInput.copy { screenOnSecs = it } }
})
}
item {
DropDownPreference(title = "GPS coordinates format",
enabled = connected,
items = ConfigProtos.Config.DisplayConfig.GpsCoordinateFormat.values()
.filter { it != ConfigProtos.Config.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",
value = displayInput.autoScreenCarouselSecs.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { displayInput = displayInput.copy { autoScreenCarouselSecs = it } }
})
}
item {
SwitchPreference(
title = "Compass north top",
checked = displayInput.compassNorthTop,
enabled = connected,
onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } })
}
item { Divider() }
item {
SwitchPreference(
title = "Flip screen",
checked = displayInput.flipScreen,
enabled = connected,
onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Display units",
enabled = connected,
items = ConfigProtos.Config.DisplayConfig.DisplayUnits.values()
.filter { it != ConfigProtos.Config.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 = connected,
// items = ConfigProtos.Config.DisplayConfig.OledType.values()
// .filter { it != ConfigProtos.Config.DisplayConfig.OledType.UNRECOGNIZED }
// .map { it to it.name },
// selectedItem = displayInput.oled,
// onItemSelected = { displayInput = displayInput.copy { oled = it } })
// }
// item { Divider() }
item {
PreferenceFooter(
enabled = displayInput != localConfig.display,
onCancelClicked = { displayInput = localConfig.display },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateDisplayConfig { displayInput }
})
}
item { PreferenceCategory(text = "LoRa Config") }
item {
SwitchPreference(
title = "Use modem preset",
checked = loraInput.usePreset,
enabled = connected,
onCheckedChange = { loraInput = loraInput.copy { usePreset = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Modem preset",
enabled = connected && loraInput.usePreset,
items = ConfigProtos.Config.LoRaConfig.ModemPreset.values()
.filter { it != ConfigProtos.Config.LoRaConfig.ModemPreset.UNRECOGNIZED }
.map { it to it.name },
selectedItem = loraInput.modemPreset,
onItemSelected = { loraInput = loraInput.copy { modemPreset = it } })
}
item { Divider() }
item {
EditTextPreference(
title = "Bandwidth",
value = loraInput.bandwidth.uintToString(),
enabled = connected && !loraInput.usePreset,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { bandwidth = it } }
})
}
item {
EditTextPreference(
title = "Spread factor",
value = loraInput.spreadFactor.uintToString(),
enabled = connected && !loraInput.usePreset,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { spreadFactor = it } }
})
}
item {
EditTextPreference(
title = "Coding rate",
value = loraInput.codingRate.uintToString(),
enabled = connected && !loraInput.usePreset,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { codingRate = it } }
})
}
item {
EditTextPreference(
title = "Frequency offset",
value = loraInput.frequencyOffset.toString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.toFloatOrNull()
?.let { loraInput = loraInput.copy { frequencyOffset = it } }
})
}
item {
DropDownPreference(title = "Region (frequency plan)",
enabled = connected,
items = ConfigProtos.Config.LoRaConfig.RegionCode.values()
.filter { it != ConfigProtos.Config.LoRaConfig.RegionCode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = loraInput.region,
onItemSelected = { loraInput = loraInput.copy { region = it } })
}
item { Divider() }
item {
EditTextPreference(
title = "Hop limit",
value = loraInput.hopLimit.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { hopLimit = it } }
})
}
item {
SwitchPreference(
title = "TX enabled",
checked = loraInput.txEnabled,
enabled = connected,
onCheckedChange = { loraInput = loraInput.copy { txEnabled = it } })
}
item { Divider() }
item {
EditTextPreference(
title = "TX power",
value = loraInput.txPower.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { txPower = it } }
})
}
item {
EditTextPreference(
title = "Channel number",
value = loraInput.channelNum.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { loraInput = loraInput.copy { channelNum = it } }
})
}
item {
PreferenceFooter(
enabled = loraInput != localConfig.lora,
onCancelClicked = { loraInput = localConfig.lora },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateLoraConfig { loraInput }
})
}
item { PreferenceCategory(text = "Bluetooth Config") }
item {
SwitchPreference(
title = "Bluetooth enabled",
checked = bluetoothInput.enabled,
enabled = connected,
onCheckedChange = { bluetoothInput = bluetoothInput.copy { enabled = it } })
}
item { Divider() }
item {
DropDownPreference(title = "Pairing mode",
enabled = connected,
items = ConfigProtos.Config.BluetoothConfig.PairingMode.values()
.filter { it != ConfigProtos.Config.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.uintToString(),
enabled = connected,
keyboardActions = KeyboardActions(onSend = {
focusManager.clearFocus()
}),
onValueChanged = { value ->
value.stringToIntOrNull()
?.let { bluetoothInput = bluetoothInput.copy { fixedPin = it } }
})
}
item {
PreferenceFooter(
enabled = bluetoothInput != localConfig.bluetooth,
onCancelClicked = { bluetoothInput = localConfig.bluetooth },
onSaveClicked = {
focusManager.clearFocus()
viewModel.updateBluetoothConfig { bluetoothInput }
})
}
}
}

View file

@ -0,0 +1,21 @@
package com.geeksville.mesh.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.model.UIViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.viewModelFactory
import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun PreferenceScreen(viewModel: UIViewModel = viewModel()) {
PreferenceItemList(viewModel)
}
//@Preview(showBackground = true)
//@Composable
//fun PreferencePreview() {
// AppTheme {
// PreferenceScreen(viewModel(factory = viewModelFactory { }))
// }
//}

View file

@ -0,0 +1,86 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun <T> DropDownPreference(
title: String,
enabled: Boolean,
items: List<Pair<T, String>>,
selectedItem: T,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
) {
var dropDownExpanded by remember { mutableStateOf(value = false) }
RegularPreference(
title = title,
subtitle = items.first { it.first == selectedItem }.second,
onClick = {
dropDownExpanded = true
},
modifier = modifier
.background(
color = if (dropDownExpanded)
MaterialTheme.colors.primary.copy(alpha = 0.2f)
else
Color.Unspecified
),
enabled = enabled,
)
Box {
DropdownMenu(
expanded = dropDownExpanded,
onDismissRequest = { dropDownExpanded = !dropDownExpanded },
) {
items.forEach { item ->
DropdownMenuItem(
onClick = {
dropDownExpanded = false
onItemSelected(item.first)
},
modifier = Modifier
.background(
color = if (selectedItem == item.first)
MaterialTheme.colors.primary.copy(alpha = 0.3f)
else
Color.Unspecified,
),
content = {
Text(
text = item.second,
overflow = TextOverflow.Ellipsis,
)
}
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun DropDownPreferencePreview() {
DropDownPreference(
title = "Settings",
enabled = true,
items = listOf("TEST1" to "text1", "TEST2" to "text2"),
selectedItem = "TEST2",
onItemSelected = {}
)
}

View file

@ -0,0 +1,68 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
@Composable // Default keyboardOptions: KeyboardType.Number, ImeAction.Send
fun EditTextPreference(
title: String,
value: String,
enabled: Boolean,
keyboardActions: KeyboardActions,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
EditTextPreference(
title = title,
value = value,
enabled = enabled,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number, imeAction = ImeAction.Send
),
keyboardActions = keyboardActions,
onValueChanged = onValueChanged,
modifier = modifier
)
}
@Composable
fun EditTextPreference(
title: String,
value: String,
enabled: Boolean,
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
TextField(
value = value,
singleLine = true,
modifier = modifier.fillMaxWidth(),
enabled = enabled,
onValueChange = onValueChanged,
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
@Preview(showBackground = true)
@Composable
private fun EditTextPreferencePreview() {
EditTextPreference(
title = "Advanced Settings",
value = "${UInt.MAX_VALUE}",
enabled = true,
keyboardActions = KeyboardActions {},
onValueChanged = {}
)
}

View file

@ -0,0 +1,29 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceCategory(
text: String,
modifier: Modifier = Modifier
) {
Text(
text,
modifier = modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp, end = 16.dp),
style = MaterialTheme.typography.h6,
)
}
@Preview(showBackground = true)
@Composable
private fun PreferenceCategoryPreview() {
PreferenceCategory(
text = "Advanced settings"
)
}

View file

@ -0,0 +1,63 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
@Composable
fun PreferenceFooter(
enabled: Boolean,
onCancelClicked: () -> Unit,
onSaveClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.size(48.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
Button(
modifier = modifier
.fillMaxWidth()
.weight(1f),
enabled = enabled,
onClick = onCancelClicked,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Red)
) {
Text(
text = stringResource(id = R.string.cancel),
style = MaterialTheme.typography.body1,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
)
}
Button(
modifier = modifier
.fillMaxWidth()
.weight(1f),
enabled = enabled,
onClick = onSaveClicked,
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Green)
) {
Text(
text = stringResource(id = R.string.save_btn),
style = MaterialTheme.typography.body1,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.DarkGray,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PreferenceFooterPreview() {
PreferenceFooter(enabled = true, onCancelClicked = {}, onSaveClicked = {})
}

View file

@ -0,0 +1,73 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun RegularPreference(
title: String,
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
RegularPreference(
title = title,
subtitle = AnnotatedString(text = subtitle),
onClick = onClick,
modifier = modifier,
enabled = enabled
)
}
@Composable
fun RegularPreference(
title: String,
subtitle: AnnotatedString,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Column(
modifier = modifier
.fillMaxWidth()
.clickable(
enabled = enabled,
onClick = onClick,
)
.padding(all = 16.dp),
) {
Text(
text = title,
style = MaterialTheme.typography.body1,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
)
Text(
text = subtitle,
style = MaterialTheme.typography.body2,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium),
)
}
}
@Preview(showBackground = true)
@Composable
private fun RegularPreferencePreview() {
RegularPreference(
title = "Advanced settings",
subtitle = AnnotatedString(text = "Lorem ipsum dolor sit amet"),
onClick = { },
)
}

View file

@ -0,0 +1,60 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
@Composable
fun SwitchPreference(
title: String,
checked: Boolean,
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.size(48.dp)
.padding(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.body2,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
)
Switch(
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
enabled = enabled,
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
uncheckedThumbColor = colorResource(R.color.colourGrey)
)
)
}
}
@Preview(showBackground = true)
@Composable
private fun SwitchPreferencePreview() {
SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {})
}

View file

@ -0,0 +1,18 @@
package com.geeksville.mesh.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val LightGray = Color(0xFFFAFAFA)
val LightSkyBlue = Color(0x99A6D1E6)
val LightBlue = Color(0xFFA6D1E6)
val SkyBlue = Color(0xFF57AEFF)
val LightPink = Color(0xFFFFE6E6)
val LightGreen = Color(0xFFCFE8A9)
val LightRed = Color(0xFFFFB3B3)
val MeshtasticGreen = Color(0xFF67EA94)

View file

@ -0,0 +1,11 @@
package com.geeksville.mesh.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View file

@ -0,0 +1,47 @@
package com.geeksville.mesh.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = SkyBlue,
primaryVariant = LightSkyBlue,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View file

@ -0,0 +1,41 @@
package com.geeksville.mesh.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
h3 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
h4 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
),
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
body2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
),
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)