From 4bcd408dce1f94c52e7bdd9e1b585821fecdaf3c Mon Sep 17 00:00:00 2001 From: Andre K Date: Tue, 8 Nov 2022 23:11:18 -0300 Subject: [PATCH] add user & device config settings (#520) * add MeshUser & LocalConfig prefs --- .../com/geeksville/mesh/IMeshService.aidl | 2 +- .../main/java/com/geeksville/mesh/NodeInfo.kt | 5 +- .../geeksville/mesh/model/ChannelOption.kt | 20 +- .../java/com/geeksville/mesh/model/NodeDB.kt | 6 +- .../java/com/geeksville/mesh/model/UIState.kt | 31 +- .../geeksville/mesh/service/MeshService.kt | 13 +- .../mesh/ui/AdvancedSettingsFragment.kt | 101 +-- .../com/geeksville/mesh/ui/ChannelFragment.kt | 1 - .../geeksville/mesh/ui/PreferenceItemList.kt | 807 ++++++++++++++++++ .../geeksville/mesh/ui/PreferenceScreen.kt | 21 + .../mesh/ui/components/DropDownPreference.kt | 86 ++ .../mesh/ui/components/EditTextPreference.kt | 68 ++ .../mesh/ui/components/PreferenceCategory.kt | 29 + .../mesh/ui/components/PreferenceFooter.kt | 63 ++ .../mesh/ui/components/RegularPreference.kt | 73 ++ .../mesh/ui/components/SwitchPreference.kt | 60 ++ .../com/geeksville/mesh/ui/theme/Color.kt | 18 + .../com/geeksville/mesh/ui/theme/Shape.kt | 11 + .../com/geeksville/mesh/ui/theme/Theme.kt | 47 + .../java/com/geeksville/mesh/ui/theme/Type.kt | 41 + app/src/main/res/layout/advanced_settings.xml | 62 +- app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-ko-rKR/strings.xml | 2 - app/src/main/res/values-pl/strings.xml | 2 - app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values-pt/strings.xml | 2 - app/src/main/res/values-sk/strings.xml | 2 - app/src/main/res/values-zh/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - 30 files changed, 1383 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/DropDownPreference.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/PreferenceCategory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/PreferenceFooter.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/RegularPreference.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/SwitchPreference.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/theme/Color.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/theme/Shape.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/geeksville/mesh/ui/theme/Type.kt diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index bb310e092..82177476d 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -49,7 +49,7 @@ interface IMeshService { If myId is null, then the existing unique node ID is preserved, only the human visible longName/shortName is changed */ - void setOwner(String myId, String longName, String shortName); + void setOwner(String myId, String longName, String shortName, boolean isLicensed); /// Return my unique user ID string String getMyId(); diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index 87d3067b0..c164a0d3e 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt index 344573558..cd50750b3 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index 877e35a75..6efbe8876 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -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 ) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 7adf827b8..b66e96f37 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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() @@ -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}") diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 928779eec..c50b18ce1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt index fdb81614c..a936190ca 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt @@ -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") - } - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 5684be78a..5fa9cb6b2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt new file mode 100644 index 000000000..62e6c8e14 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/PreferenceItemList.kt @@ -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 } + }) + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt new file mode 100644 index 000000000..750113f9c --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/PreferenceScreen.kt @@ -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 { })) +// } +//} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/DropDownPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/DropDownPreference.kt new file mode 100644 index 000000000..8780023bc --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/DropDownPreference.kt @@ -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 DropDownPreference( + title: String, + enabled: Boolean, + items: List>, + 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 = {} + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt new file mode 100644 index 000000000..f669f73ae --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt @@ -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 = {} + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceCategory.kt b/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceCategory.kt new file mode 100644 index 000000000..8851c99c1 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceCategory.kt @@ -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" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceFooter.kt b/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceFooter.kt new file mode 100644 index 000000000..cf342d0db --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/PreferenceFooter.kt @@ -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 = {}) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/RegularPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/RegularPreference.kt new file mode 100644 index 000000000..48abbc34c --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/RegularPreference.kt @@ -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 = { }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/SwitchPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/SwitchPreference.kt new file mode 100644 index 000000000..24bca26b9 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/SwitchPreference.kt @@ -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 = {}) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/theme/Color.kt b/app/src/main/java/com/geeksville/mesh/ui/theme/Color.kt new file mode 100644 index 000000000..adc0e933b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/theme/Color.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/theme/Shape.kt b/app/src/main/java/com/geeksville/mesh/ui/theme/Shape.kt new file mode 100644 index 000000000..99ce8e696 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/theme/Shape.kt @@ -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) +) diff --git a/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt b/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt new file mode 100644 index 000000000..0646ca402 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/theme/Type.kt b/app/src/main/java/com/geeksville/mesh/ui/theme/Type.kt new file mode 100644 index 000000000..91432aa78 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/theme/Type.kt @@ -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 + ) + */ +) diff --git a/app/src/main/res/layout/advanced_settings.xml b/app/src/main/res/layout/advanced_settings.xml index b8ffe2bea..94ff4e872 100644 --- a/app/src/main/res/layout/advanced_settings.xml +++ b/app/src/main/res/layout/advanced_settings.xml @@ -6,64 +6,10 @@ android:layout_height="match_parent" android:background="@color/colorAdvancedBackground"> - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 321a18217..27a043932 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -73,10 +73,8 @@ tiempo de recepción del mensaje estado de recepción de mensajes Estado de entrega del mensaje - Periodo de emisión de la posición (en segundos) Período de reposo del dispositivo (en segundos) Notificaciones de mensajes - El periodo mínimo de emisión de este canal es %d Protocolo de prueba de esfuerzo Configuración avanzada Es necesario actualizar el firmware diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 24ac83719..b08aeae42 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -73,10 +73,8 @@ üzenet fogadásának ideje üzenet fogadásának állapota Üzenet kézbesítésének állapota - Pozíció hírdetésének gyakorisága (másodpercben) Eszköz alvásának gyakorisága (másodpercben) Értesítések az üzenetekről - Minimum üzenet küldési gyakoriság ezen a csatornán %d Protokoll stressz teszt Haladó beállítások Firmware frissítés szükséges diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 13f95e8f8..fdbceb527 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -79,10 +79,8 @@ 메시지 수신 시간 메시지 수신 상태 메시지 전송 상태 - 위치 전송 주기 (in seconds) 기기가 절전모드 들어가기까지의 시간 (in seconds) 메시지 알림 - 이 채널의 최소 전송 주기는 %d 입니다. 프로토콜 스트레스 테스트 고급 설정 펌웨어 업데이트 필요 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a0d81a657..63bf9d8d7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -74,10 +74,8 @@ czas odbioru wiadomości stan odbioru wiadomości Status doręczenia wiadomości - Okres pozycji transmisji (w sekundach) Okres uśpienia urządzenia (w sekundach) Powiadomienia o wiadomościach - Minimalny okres nadawania dla tego kanału to %d Protokół testu warunków skrajnych Zaawansowane ustawienia Wymagana aktualizacja oprogramowania układowego diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8393a968e..2d359f9ee 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -74,10 +74,8 @@ tempo de recebimento de mensagem estado de recebimento de mensagem Status de entrega de mensagem - Intervalo de localização (em segundos) Intervalo de suspensão (sleep) (em segundos) Notificações sobre mensagens - Período mínimo de transmissão para este canal é %d Stress test do protocolo Configurações avançadas Atualização do firmware necessária diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 4634d7a33..2408b7bc2 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -74,10 +74,8 @@ tempo de recebimento de mensagem estado de recebimento de mensagem Status de entrega de mensagem - Intervalo de localização (segundos) Intervalo de suspensão (sleep) (segundos) Notificações sobre mensagens - Período mínimo de transmissão para este canal é %d Stress test do protocolo Configurações avançadas Atualização do firmware necessária diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a263e12bc..3bc2e553f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -74,10 +74,8 @@ čas prijatia správy stav prijatia správy stav doručenia správy - Interval rozosielania pozície (v sekundách) Interval uspávania zariadenia (v sekundách) Upozornenia na správy - Najkratší interval rozosielania pre tento kanál je %d Stres test protokolu Rozšírené nastavenia Nutná aktualizácia firmvéru vysielača diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 91f9b1c0a..d6bf80663 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -74,10 +74,8 @@ 消息接收时间 消息接收状态 消息传递状态 - 广播周期(以秒为单位) 设备休眠时间(以秒为单位) 关于消息的通知 - 此频道的最短广播时间为 %d 协议压力测试 高级设置 需要固件更新 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7136aa376..c7ad5bfe5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,10 +78,8 @@ message reception time message reception state Message delivery status - Broadcast position period (in seconds) Device sleep period (in seconds) Notifications about messages - Minimum broadcast period for this channel is %d Protocol stress test Advanced settings Firmware update required