diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/DebugFiltersTest.kt b/app/src/androidTest/java/com/geeksville/mesh/compose/DebugFiltersTest.kt index c4b18acc1..e5f210621 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/compose/DebugFiltersTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/compose/DebugFiltersTest.kt @@ -17,51 +17,56 @@ package com.geeksville.mesh.compose +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import com.geeksville.mesh.R +import com.geeksville.mesh.ui.debug.FilterMode import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import androidx.test.platform.app.InstrumentationRegistry -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import com.geeksville.mesh.ui.debug.FilterMode @RunWith(AndroidJUnit4::class) class DebugFiltersTest { - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() @Test fun debugFilterBar_showsFilterButtonAndMenu() { val context = InstrumentationRegistry.getInstrumentation().targetContext val filterLabel = context.getString(R.string.debug_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } + var filterTexts by + androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } var customFilterText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("") } val presetFilters = listOf("Error", "Warning", "Info") - val logs = listOf( - com.geeksville.mesh.model.DebugViewModel.UiMeshLog( - uuid = "1", - messageType = "Info", - formattedReceivedDate = "2024-01-01 12:00:00", - logMessage = "Sample log message" + val logs = + listOf( + com.geeksville.mesh.model.DebugViewModel.UiMeshLog( + uuid = "1", + messageType = "Info", + formattedReceivedDate = "2024-01-01 12:00:00", + logMessage = "Sample log message", + ), ) - ) com.geeksville.mesh.ui.debug.DebugFilterBar( filterTexts = filterTexts, onFilterTextsChange = { filterTexts = it }, customFilterText = customFilterText, onCustomFilterTextChange = { customFilterText = it }, presetFilters = presetFilters, - logs = logs + logs = logs, ) } // The filter button should be visible @@ -73,27 +78,32 @@ class DebugFiltersTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val activeFiltersLabel = context.getString(R.string.debug_active_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } + var filterTexts by + androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } var customFilterText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("") } - com.geeksville.mesh.ui.debug.DebugActiveFilters( - filterTexts = filterTexts, - onFilterTextsChange = { filterTexts = it }, - filterMode = FilterMode.OR, - onFilterModeChange = {} - ) - com.geeksville.mesh.ui.debug.DebugCustomFilterInput( - customFilterText = customFilterText, - onCustomFilterTextChange = { customFilterText = it }, - filterTexts = filterTexts, - onFilterTextsChange = { filterTexts = it }, - ) + Column(modifier = Modifier.padding(16.dp)) { + com.geeksville.mesh.ui.debug.DebugActiveFilters( + filterTexts = filterTexts, + onFilterTextsChange = { filterTexts = it }, + filterMode = FilterMode.OR, + onFilterModeChange = {}, + ) + com.geeksville.mesh.ui.debug.DebugCustomFilterInput( + customFilterText = customFilterText, + onCustomFilterTextChange = { customFilterText = it }, + filterTexts = filterTexts, + onFilterTextsChange = { filterTexts = it }, + ) + } + } + with(composeTestRule) { + // Add a custom filter + onNodeWithText("Add custom filter").performTextInput("MyFilter") + onNodeWithContentDescription("Add filter").performClick() + // The active filters label and the filter chip should be visible + onNodeWithText(activeFiltersLabel).assertIsDisplayed() + onNodeWithText("MyFilter").assertIsDisplayed() } - // Add a custom filter - composeTestRule.onNodeWithText("Add custom filter").performTextInput("MyFilter") - composeTestRule.onNodeWithContentDescription("Add filter").performClick() - // The active filters label and the filter chip should be visible - composeTestRule.onNodeWithText(activeFiltersLabel).assertIsDisplayed() - composeTestRule.onNodeWithText("MyFilter").assertIsDisplayed() } @Test @@ -101,12 +111,13 @@ class DebugFiltersTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val activeFiltersLabel = context.getString(R.string.debug_active_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf("A", "B")) } + var filterTexts by + androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf("A", "B")) } com.geeksville.mesh.ui.debug.DebugActiveFilters( filterTexts = filterTexts, onFilterTextsChange = { filterTexts = it }, filterMode = FilterMode.OR, - onFilterModeChange = {} + onFilterModeChange = {}, ) } // The active filters label and chips should be visible @@ -119,4 +130,4 @@ class DebugFiltersTest { composeTestRule.onNodeWithText("A").assertDoesNotExist() composeTestRule.onNodeWithText("B").assertDoesNotExist() } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/DebugSearchTest.kt b/app/src/androidTest/java/com/geeksville/mesh/compose/DebugSearchTest.kt index ba14c1202..dccc57fa1 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/compose/DebugSearchTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/compose/DebugSearchTest.kt @@ -17,34 +17,33 @@ package com.geeksville.mesh.compose +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.printToLog -import androidx.compose.ui.test.printToString +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import com.geeksville.mesh.R import com.geeksville.mesh.model.LogSearchManager.SearchState import com.geeksville.mesh.ui.debug.DebugSearchBar +import com.geeksville.mesh.ui.debug.FilterMode import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import androidx.test.platform.app.InstrumentationRegistry -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import com.geeksville.mesh.ui.debug.FilterMode -import com.geeksville.mesh.ui.debug.DebugActiveFilters @RunWith(AndroidJUnit4::class) class DebugSearchTest { - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() @Test fun debugSearchBar_showsPlaceholder() { @@ -56,7 +55,7 @@ class DebugSearchTest { onSearchTextChange = {}, onNextMatch = {}, onPreviousMatch = {}, - onClearSearch = {} + onClearSearch = {}, ) } composeTestRule.onNodeWithText(placeholder).assertIsDisplayed() @@ -67,17 +66,16 @@ class DebugSearchTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val placeholder = context.getString(R.string.debug_default_search) composeTestRule.setContent { - var searchText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("test") } + var searchText by androidx.compose.runtime.remember { mutableStateOf("test") } DebugSearchBar( searchState = SearchState(searchText = searchText), onSearchTextChange = { searchText = it }, onNextMatch = {}, onPreviousMatch = {}, - onClearSearch = { searchText = "" } + onClearSearch = { searchText = "" }, ) } - composeTestRule.onNodeWithContentDescription("Clear search").assertIsDisplayed() - .performClick() + composeTestRule.onNodeWithContentDescription("Clear search").assertIsDisplayed().performClick() composeTestRule.onNodeWithText(placeholder).assertIsDisplayed() } @@ -89,16 +87,20 @@ class DebugSearchTest { composeTestRule.setContent { DebugSearchBar( - searchState = SearchState( + searchState = + SearchState( searchText = searchText, currentMatchIndex = currentMatchIndex, - allMatches = List(matchCount) { com.geeksville.mesh.model.LogSearchManager.SearchMatch(it, 0, 6, "Packet") }, - hasMatches = true + allMatches = + List(matchCount) { + com.geeksville.mesh.model.LogSearchManager.SearchMatch(it, 0, 6, "Packet") + }, + hasMatches = true, ), onSearchTextChange = {}, onNextMatch = {}, onPreviousMatch = {}, - onClearSearch = {} + onClearSearch = {}, ) } // Check the match count display (e.g., '2/3') @@ -115,24 +117,25 @@ class DebugSearchTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val filterLabel = context.getString(R.string.debug_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } - var customFilterText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("") } + var filterTexts by androidx.compose.runtime.remember { mutableStateOf(listOf()) } + var customFilterText by androidx.compose.runtime.remember { mutableStateOf("") } val presetFilters = listOf("Error", "Warning", "Info") - val logs = listOf( - com.geeksville.mesh.model.DebugViewModel.UiMeshLog( - uuid = "1", - messageType = "Info", - formattedReceivedDate = "2024-01-01 12:00:00", - logMessage = "Sample log message" + val logs = + listOf( + com.geeksville.mesh.model.DebugViewModel.UiMeshLog( + uuid = "1", + messageType = "Info", + formattedReceivedDate = "2024-01-01 12:00:00", + logMessage = "Sample log message", + ), ) - ) com.geeksville.mesh.ui.debug.DebugFilterBar( filterTexts = filterTexts, onFilterTextsChange = { filterTexts = it }, customFilterText = customFilterText, onCustomFilterTextChange = { customFilterText = it }, presetFilters = presetFilters, - logs = logs + logs = logs, ) } // The filter button should be visible @@ -144,27 +147,29 @@ class DebugSearchTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val activeFiltersLabel = context.getString(R.string.debug_active_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf()) } - var customFilterText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("") } - com.geeksville.mesh.ui.debug.DebugActiveFilters( - filterTexts = filterTexts, - onFilterTextsChange = { filterTexts = it }, - filterMode = FilterMode.OR, - onFilterModeChange = {} - ) - com.geeksville.mesh.ui.debug.DebugCustomFilterInput( - customFilterText = customFilterText, - onCustomFilterTextChange = { customFilterText = it }, - filterTexts = filterTexts, - onFilterTextsChange = { filterTexts = it } - ) + var filterTexts by androidx.compose.runtime.remember { mutableStateOf(listOf()) } + var customFilterText by androidx.compose.runtime.remember { mutableStateOf("") } + Column(modifier = Modifier.padding(16.dp)) { + com.geeksville.mesh.ui.debug.DebugActiveFilters( + filterTexts = filterTexts, + onFilterTextsChange = { filterTexts = it }, + filterMode = FilterMode.OR, + onFilterModeChange = {}, + ) + com.geeksville.mesh.ui.debug.DebugCustomFilterInput( + customFilterText = customFilterText, + onCustomFilterTextChange = { customFilterText = it }, + filterTexts = filterTexts, + onFilterTextsChange = { filterTexts = it }, + ) + } + } + with(composeTestRule) { + onNodeWithText("Add custom filter").performTextInput("MyFilter") + onNodeWithContentDescription("Add filter").performClick() + onNodeWithText(activeFiltersLabel).assertIsDisplayed() + onNodeWithText("MyFilter").assertIsDisplayed() } - // Add a custom filter - composeTestRule.onNodeWithText("Add custom filter").performTextInput("MyFilter") - composeTestRule.onNodeWithContentDescription("Add filter").performClick() - // The active filters label and the filter chip should be visible - composeTestRule.onNodeWithText(activeFiltersLabel).assertIsDisplayed() - composeTestRule.onNodeWithText("MyFilter").assertIsDisplayed() } @Test @@ -172,12 +177,12 @@ class DebugSearchTest { val context = InstrumentationRegistry.getInstrumentation().targetContext val activeFiltersLabel = context.getString(R.string.debug_active_filters) composeTestRule.setContent { - var filterTexts by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf("A", "B")) } + var filterTexts by androidx.compose.runtime.remember { mutableStateOf(listOf("A", "B")) } com.geeksville.mesh.ui.debug.DebugActiveFilters( filterTexts = filterTexts, onFilterTextsChange = { filterTexts = it }, filterMode = FilterMode.OR, - onFilterModeChange = {} + onFilterModeChange = {}, ) } // The active filters label and chips should be visible @@ -190,4 +195,4 @@ class DebugSearchTest { composeTestRule.onNodeWithText("A").assertDoesNotExist() composeTestRule.onNodeWithText("B").assertDoesNotExist() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt index fe0ffb038..54c8ba7cc 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/SettingsNavigation.kt @@ -219,6 +219,7 @@ private fun NavGraphBuilder.configRoutesScreens(navController: NavHostController entry.name, entry.screenComposable, ) + else -> Unit // Should not happen if ConfigRoute enum is exhaustive for this context } } @@ -374,7 +375,7 @@ enum class ConfigRoute( ; companion object { - fun filterExcludedFrom(metadata: DeviceMetadata?): List = entries.filter { + private fun filterExcludedFrom(metadata: DeviceMetadata?): List = entries.filter { when { metadata == null -> true // Include all routes if metadata is null it == BLUETOOTH -> metadata.hasBluetooth @@ -382,6 +383,11 @@ enum class ConfigRoute( else -> true // Include all other routes by default } } + + val radioConfigRoutes = listOf(LORA, CHANNELS, SECURITY) + + fun deviceConfigRoutes(metadata: DeviceMetadata?): List = + filterExcludedFrom(metadata) - radioConfigRoutes } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/BitwisePreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/BitwisePreference.kt index 0cde88f73..c5aab4368 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/BitwisePreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/BitwisePreference.kt @@ -17,15 +17,16 @@ package com.geeksville.mesh.ui.common.components -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.twotone.KeyboardArrowDown -import androidx.compose.material.icons.twotone.KeyboardArrowUp import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -36,8 +37,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.geeksville.mesh.R +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BitwisePreference( title: String, @@ -46,44 +49,43 @@ fun BitwisePreference( items: List>, onItemSelected: (Int) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, ) { - var dropDownExpanded by remember { mutableStateOf(value = false) } + var expanded by remember { mutableStateOf(false) } - RegularPreference( - title = title, - subtitle = value.toString(), - onClick = { dropDownExpanded = !dropDownExpanded }, - enabled = enabled, - trailingIcon = if (dropDownExpanded) { - Icons.TwoTone.KeyboardArrowUp - } else { - Icons.TwoTone.KeyboardArrowDown + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + if (enabled) { + expanded = !expanded + } }, - ) - - Box { - DropdownMenu( - expanded = dropDownExpanded, - onDismissRequest = { dropDownExpanded = !dropDownExpanded }, - ) { + modifier = modifier.padding(vertical = 8.dp), + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled), + readOnly = true, + value = value.toString(), + onValueChange = {}, + label = { Text(title) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + enabled = enabled, + supportingText = { if (summary != null) Text(text = summary) }, + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { items.forEach { item -> DropdownMenuItem( - onClick = { onItemSelected(value xor item.first) }, - modifier = modifier.fillMaxWidth(), text = { - Text( - text = item.second, - overflow = TextOverflow.Ellipsis, - ) + Text(text = item.second, overflow = TextOverflow.Ellipsis) Checkbox( - modifier = modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.End), + modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End), checked = value and item.first != 0, onCheckedChange = { onItemSelected(value xor item.first) }, enabled = enabled, ) - } + }, + onClick = { onItemSelected(value xor item.first) }, ) } PreferenceFooter( @@ -91,7 +93,7 @@ fun BitwisePreference( negativeText = R.string.clear, onNegativeClicked = { onItemSelected(0) }, positiveText = R.string.close, - onPositiveClicked = { dropDownExpanded = false }, + onPositiveClicked = { expanded = false }, ) } } @@ -103,8 +105,9 @@ private fun BitwisePreferencePreview() { BitwisePreference( title = "Settings", value = 3, + summary = "This is a summary", enabled = true, items = listOf(1 to "TEST1", 2 to "TEST2"), - onItemSelected = {} + onItemSelected = {}, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt index 68a0f65ab..0cb517b22 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/DropDownPreference.kt @@ -17,13 +17,14 @@ package com.geeksville.mesh.ui.common.components -import androidx.compose.foundation.background -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.twotone.KeyboardArrowDown -import androidx.compose.material.icons.twotone.KeyboardArrowUp -import androidx.compose.material3.DropdownMenu +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -31,11 +32,8 @@ 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.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.geeksville.mesh.R +import androidx.compose.ui.unit.dp import com.google.protobuf.ProtocolMessageEnum @Composable @@ -50,8 +48,9 @@ fun > DropDownPreference( DropDownPreference( title = title, enabled = enabled, - items = selectedItem.declaringJavaClass.enumConstants - ?.filter { it.name != "UNRECOGNIZED" }?.map { it to it.name } ?: emptyList(), + items = + selectedItem.declaringJavaClass.enumConstants?.filter { it.name != "UNRECOGNIZED" }?.map { it to it.name } + ?: emptyList(), selectedItem = selectedItem, onItemSelected = onItemSelected, modifier = modifier, @@ -59,6 +58,7 @@ fun > DropDownPreference( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun DropDownPreference( title: String, @@ -69,7 +69,7 @@ fun DropDownPreference( modifier: Modifier = Modifier, summary: String? = null, ) { - var dropDownExpanded by remember { mutableStateOf(value = false) } + var expanded by remember { mutableStateOf(false) } val deprecatedItems: List = remember { if (selectedItem is ProtocolMessageEnum) { @@ -77,58 +77,46 @@ fun DropDownPreference( val descriptor = (selectedItem as ProtocolMessageEnum).descriptorForType @Suppress("UNCHECKED_CAST") - enum?.filter { entries -> - descriptor.values.any { it.name == entries.name && it.options.deprecated } - } as? List ?: emptyList() // Safe cast to List or return emptyList if cast fails + enum?.filter { entries -> descriptor.values.any { it.name == entries.name && it.options.deprecated } } + as? List ?: emptyList() // Safe cast to List or return emptyList if cast fails } else { emptyList() } } - RegularPreference( - title = title, - subtitle = items.find { it.first == selectedItem }?.second - ?: stringResource(id = R.string.unrecognized), - onClick = { - dropDownExpanded = true + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + if (enabled) { + expanded = !expanded + } }, - enabled = enabled, - trailingIcon = if (dropDownExpanded) { - Icons.TwoTone.KeyboardArrowUp - } else { - Icons.TwoTone.KeyboardArrowDown - }, - summary = summary, - dropdownMenu = { - DropdownMenu( - expanded = dropDownExpanded, - onDismissRequest = { dropDownExpanded = !dropDownExpanded }, - ) { - items.filterNot { it.first in deprecatedItems }.forEach { item -> + modifier = modifier.padding(vertical = 8.dp), + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled), + readOnly = true, + value = items.firstOrNull { it.first == selectedItem }?.second ?: "", + onValueChange = {}, + label = { Text(title) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + enabled = enabled, + supportingText = { if (summary != null) Text(text = summary) }, + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + items + .filterNot { it.first in deprecatedItems } + .forEach { selectionOption -> DropdownMenuItem( + text = { Text(selectionOption.second) }, onClick = { - dropDownExpanded = false - onItemSelected(item.first) + onItemSelected(selectionOption.first) + expanded = false }, - modifier = modifier - .background( - color = if (selectedItem == item.first) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) - } else { - Color.Unspecified - }, - ), - text = { - Text( - text = item.second, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - } ) } - } } - ) + } } @Preview(showBackground = true) @@ -140,6 +128,6 @@ private fun DropDownPreferencePreview() { enabled = true, items = listOf("TEST1" to "text1", "TEST2" to "text2"), selectedItem = "TEST2", - onItemSelected = {} + onItemSelected = {}, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt index 8ae1da409..ebce4ff70 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt @@ -17,6 +17,7 @@ package com.geeksville.mesh.ui.common.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions @@ -54,6 +55,7 @@ import org.meshtastic.core.model.Channel fun EditBase64Preference( modifier: Modifier = Modifier, title: String, + summary: String? = null, value: ByteString, enabled: Boolean, readOnly: Boolean = false, @@ -79,50 +81,59 @@ fun EditBase64Preference( onGenerateKey != null && !isFocused -> Icons.TwoTone.Refresh to stringResource(R.string.reset) else -> null to null } - - OutlinedTextField( - value = valueState, - onValueChange = { - valueState = it - runCatching { it.toByteString() }.onSuccess(onValueChange) - }, - modifier = modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused }, - enabled = enabled, - readOnly = readOnly, - label = { Text(text = title) }, - isError = isError, - keyboardOptions = - KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), - keyboardActions = keyboardActions, - trailingIcon = { - if (icon != null) { - IconButton( - onClick = { - if (isError) { - valueState = value.encodeToString() - onValueChange(value) - } else if (onGenerateKey != null && !isFocused) { - onGenerateKey() - } - }, - enabled = enabled, - ) { - Icon( - imageVector = icon, - contentDescription = description, - tint = - if (isError) { - MaterialTheme.colorScheme.error - } else { - LocalContentColor.current + Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + OutlinedTextField( + value = valueState, + onValueChange = { + valueState = it + runCatching { it.toByteString() }.onSuccess(onValueChange) + }, + modifier = Modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused }, + enabled = enabled, + readOnly = readOnly, + label = { Text(text = title) }, + isError = isError, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), + keyboardActions = keyboardActions, + trailingIcon = { + if (icon != null) { + IconButton( + onClick = { + if (isError) { + valueState = value.encodeToString() + onValueChange(value) + } else if (onGenerateKey != null && !isFocused) { + onGenerateKey() + } }, - ) + enabled = enabled, + ) { + Icon( + imageVector = icon, + contentDescription = description, + tint = + if (isError) { + MaterialTheme.colorScheme.error + } else { + LocalContentColor.current + }, + ) + } + } else if (trailingIcon != null) { + trailingIcon() } - } else if (trailingIcon != null) { - trailingIcon() - } - }, - ) + }, + ) + if (summary != null) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + } } @Preview(showBackground = true) @@ -130,6 +141,7 @@ fun EditBase64Preference( private fun EditBase64PreferencePreview() { EditBase64Preference( title = "Title", + summary = "This is a summary", value = Channel.getRandomKey(), enabled = true, keyboardActions = KeyboardActions {}, diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditListPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditListPreference.kt index d305a1d2e..17c906a94 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditListPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditListPreference.kt @@ -47,6 +47,7 @@ import com.geeksville.mesh.copy import com.geeksville.mesh.remoteHardwarePin import com.google.protobuf.ByteString +@Suppress("LongMethod") @Composable inline fun EditListPreference( title: String, @@ -56,12 +57,21 @@ inline fun EditListPreference( keyboardActions: KeyboardActions, crossinline onValuesChanged: (List) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, ) { val focusManager = LocalFocusManager.current val listState = remember(list) { mutableStateListOf().apply { addAll(list) } } - Column(modifier = modifier) { - Text(modifier = modifier.padding(16.dp), text = title, style = MaterialTheme.typography.bodyMedium) + Column(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text(text = title, style = MaterialTheme.typography.titleLarge) + if (summary != null) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } listState.forEachIndexed { index, value -> val trailingIcon = @Composable { @@ -80,80 +90,75 @@ inline fun EditListPreference( } } - // handle lora.ignoreIncoming: List - if (value is Int) { - EditTextPreference( - title = "${index + 1}/$maxCount", - value = value, - enabled = enabled, - keyboardActions = keyboardActions, - onValueChanged = { - listState[index] = it as T - onValuesChanged(listState) - }, - modifier = modifier.fillMaxWidth(), - trailingIcon = trailingIcon, - ) - } - - // handle security.adminKey: List - if (value is ByteString) { - EditBase64Preference( - title = "${index + 1}/$maxCount", - value = value, - enabled = enabled, - keyboardActions = keyboardActions, - onValueChange = { - listState[index] = it as T - onValuesChanged(listState) - }, - modifier = modifier.fillMaxWidth(), - trailingIcon = trailingIcon, - ) - } - - // handle remoteHardware.availablePins: List - if (value is RemoteHardwarePin) { - EditTextPreference( - title = stringResource(R.string.gpio_pin), - value = value.gpioPin, - enabled = enabled, - keyboardActions = keyboardActions, - onValueChanged = { - if (it in 0..255) { - listState[index] = value.copy { gpioPin = it } as T + when (value) { + is Int -> { + EditTextPreference( + title = "${index + 1}/$maxCount", + value = value, + enabled = enabled, + keyboardActions = keyboardActions, + onValueChanged = { + listState[index] = it as T onValuesChanged(listState) - } - }, - ) - EditTextPreference( - title = stringResource(R.string.name), - value = value.name, - maxSize = 14, // name max_size:15 - enabled = enabled, - isError = false, - keyboardOptions = - KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = keyboardActions, - onValueChanged = { - listState[index] = value.copy { name = it } as T - onValuesChanged(listState) - }, - trailingIcon = trailingIcon, - ) - DropDownPreference( - title = stringResource(R.string.type), - enabled = enabled, - items = - RemoteHardwarePinType.entries - .filter { it != RemoteHardwarePinType.UNRECOGNIZED } - .map { it to it.name }, - selectedItem = value.type, - onItemSelected = { - listState[index] = value.copy { type = it } as T - onValuesChanged(listState) - }, - ) + }, + trailingIcon = trailingIcon, + ) + } + is ByteString -> { + EditBase64Preference( + title = "${index + 1}/$maxCount", + value = value, + enabled = enabled, + keyboardActions = keyboardActions, + onValueChange = { + listState[index] = it as T + onValuesChanged(listState) + }, + trailingIcon = trailingIcon, + ) + } + is RemoteHardwarePin -> { + EditTextPreference( + title = stringResource(R.string.gpio_pin), + value = value.gpioPin, + enabled = enabled, + keyboardActions = keyboardActions, + onValueChanged = { + if (it in 0..255) { + listState[index] = value.copy { gpioPin = it } as T + onValuesChanged(listState) + } + }, + ) + EditTextPreference( + title = stringResource(R.string.name), + value = value.name, + maxSize = 14, // name max_size:15 + enabled = enabled, + isError = false, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = keyboardActions, + onValueChanged = { + listState[index] = value.copy { name = it } as T + onValuesChanged(listState) + }, + trailingIcon = trailingIcon, + ) + DropDownPreference( + title = stringResource(R.string.type), + enabled = enabled, + items = + RemoteHardwarePinType.entries + .filter { it != RemoteHardwarePinType.UNRECOGNIZED } + .map { it to it.name }, + selectedItem = value.type, + onItemSelected = { + listState[index] = value.copy { type = it } as T + onValuesChanged(listState) + }, + ) + } } } OutlinedButton( @@ -182,6 +187,7 @@ private fun EditListPreferencePreview() { Column { EditListPreference( title = stringResource(R.string.ignore_incoming), + summary = "This is a summary", list = listOf(12345, 67890), maxCount = 4, enabled = true, diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditTextPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditTextPreference.kt index 97d7d2671..c1736ea42 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditTextPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditTextPreference.kt @@ -27,8 +27,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Info import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,6 +54,7 @@ fun SignedIntegerEditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Int) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, ) { @@ -63,20 +64,17 @@ fun SignedIntegerEditTextPreference( title = title, value = valueState, enabled = enabled, + summary = summary, isError = valueState.toIntOrNull() == null, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, imeAction = ImeAction.Done - ), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { valueState = it - it.toIntOrNull()?.let { int -> - onValueChanged(int) - } + it.toIntOrNull()?.let { int -> onValueChanged(int) } }, onFocusChanged = onFocusChanged, modifier = modifier, - trailingIcon = trailingIcon + trailingIcon = trailingIcon, ) } @@ -89,6 +87,7 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Int) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, ) { @@ -98,21 +97,23 @@ fun EditTextPreference( title = title, value = valueState, enabled = enabled, + summary = summary, isError = value.toUInt().toString() != valueState || isError, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, imeAction = ImeAction.Done - ), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { - if (it.isEmpty()) valueState = it - else it.toUIntOrNull()?.toInt()?.let { int -> + if (it.isEmpty()) { valueState = it - onValueChanged(int) + } else { + it.toUIntOrNull()?.toInt()?.let { int -> + valueState = it + onValueChanged(int) + } } }, onFocusChanged = onFocusChanged, modifier = modifier, - trailingIcon = trailingIcon + trailingIcon = trailingIcon, ) } @@ -124,28 +125,31 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Float) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, onFocusChanged: (FocusState) -> Unit = {}, - ) { +) { var valueState by remember(value) { mutableStateOf(value.toString()) } EditTextPreference( title = title, value = valueState, enabled = enabled, + summary = summary, isError = value.toString() != valueState, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, imeAction = ImeAction.Done - ), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { - if (it.isEmpty()) valueState = it - else it.toFloatOrNull()?.let { float -> + if (it.isEmpty()) { valueState = it - onValueChanged(float) + } else { + it.toFloatOrNull()?.let { float -> + valueState = it + onValueChanged(float) + } } }, onFocusChanged = onFocusChanged, - modifier = modifier + modifier = modifier, ) } @@ -157,6 +161,7 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Double) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, ) { var valueState by remember(value) { mutableStateOf(value.toString()) } val decimalSeparators = setOf('.', ',', '٫', '、', '·') // set of possible decimal separators @@ -165,20 +170,22 @@ fun EditTextPreference( title = title, value = valueState, enabled = enabled, + summary = summary, isError = value.toString() != valueState, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, imeAction = ImeAction.Done - ), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), keyboardActions = keyboardActions, onValueChanged = { - if (it.length <= 1 || it.first() in decimalSeparators) valueState = it - else it.toDoubleOrNull()?.let { double -> + if (it.length <= 1 || it.first() in decimalSeparators) { valueState = it - onValueChanged(double) + } else { + it.toDoubleOrNull()?.let { double -> + valueState = it + onValueChanged(double) + } } }, onFocusChanged = {}, - modifier = modifier + modifier = modifier, ) } @@ -192,6 +199,7 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (String) -> Unit, modifier: Modifier = Modifier, + summary: String? = null, maxSize: Int = 0, // max_size - 1 (in bytes) onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, @@ -199,49 +207,60 @@ fun EditTextPreference( ) { var isFocused by remember { mutableStateOf(false) } - TextField( - value = value, - singleLine = true, - modifier = modifier - .fillMaxWidth() - .onFocusEvent { isFocused = it.isFocused; onFocusChanged(it) }, - enabled = enabled, - isError = isError, - onValueChange = { - if (maxSize > 0) { - if (it.toByteArray().size <= maxSize) { + Column(modifier = modifier.padding(vertical = 8.dp)) { + OutlinedTextField( + value = value, + singleLine = true, + modifier = + Modifier.fillMaxWidth().onFocusEvent { + isFocused = it.isFocused + onFocusChanged(it) + }, + enabled = enabled, + isError = isError, + onValueChange = { + if (maxSize > 0) { + if (it.toByteArray().size <= maxSize) { + onValueChanged(it) + } + } else { onValueChanged(it) } - } else onValueChanged(it) - }, - label = { Text(title) }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - visualTransformation = visualTransformation, - trailingIcon = { - if (trailingIcon != null) { - trailingIcon() - } else if (isError) { - Icon( - imageVector = Icons.TwoTone.Info, - contentDescription = stringResource(id = R.string.error), - tint = MaterialTheme.colorScheme.error + }, + label = { Text(title) }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + trailingIcon = { + if (trailingIcon != null) { + trailingIcon() + } else if (isError) { + Icon( + imageVector = Icons.TwoTone.Info, + contentDescription = stringResource(id = R.string.error), + tint = MaterialTheme.colorScheme.error, + ) + } + }, + ) + if (summary != null) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + + if (maxSize > 0 && isFocused) { + Box(contentAlignment = Alignment.BottomEnd, modifier = Modifier.fillMaxWidth()) { + Text( + text = "${value.toByteArray().size}/$maxSize", + style = MaterialTheme.typography.bodySmall, + color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(end = 8.dp, bottom = 4.dp), ) } - }, - ) - - if (maxSize > 0 && isFocused) { - Box( - contentAlignment = Alignment.BottomEnd, - modifier = modifier.fillMaxWidth() - ) { - Text( - text = "${value.toByteArray().size}/$maxSize", - style = MaterialTheme.typography.bodySmall, - color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(end = 8.dp, bottom = 4.dp) - ) } } } @@ -253,6 +272,7 @@ private fun EditTextPreferencePreview() { EditTextPreference( title = "String", value = "Meshtastic", + summary = "This is a summary", maxSize = 39, enabled = true, isError = false, @@ -265,7 +285,7 @@ private fun EditTextPreferencePreview() { value = UInt.MAX_VALUE.toInt(), enabled = true, keyboardActions = KeyboardActions {}, - onValueChanged = {} + onValueChanged = {}, ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt index 885235c1e..1c706cd68 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt @@ -80,8 +80,18 @@ fun RadioConfigItemList( if (isManaged) { ManagedMessage() } + ConfigRoute.radioConfigRoutes.forEach { + SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { + onRouteClick(it) + } + } + } - ConfigRoute.filterExcludedFrom(state.metadata).forEach { + TitledCard(title = stringResource(R.string.device_configuration), modifier = Modifier.padding(top = 16.dp)) { + if (isManaged) { + ManagedMessage() + } + ConfigRoute.deviceConfigRoutes(state.metadata).forEach { SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt index ff8e7569a..3003cde8d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DeviceConfigItemList.kt @@ -60,33 +60,33 @@ import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.SwitchPreference import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel -private val DeviceConfig.Role.stringRes: Int +private val DeviceConfig.Role.description: Int get() = when (this) { - DeviceConfig.Role.CLIENT -> R.string.role_client - DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute - DeviceConfig.Role.ROUTER -> R.string.role_router - DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client - DeviceConfig.Role.REPEATER -> R.string.role_repeater - DeviceConfig.Role.TRACKER -> R.string.role_tracker - DeviceConfig.Role.SENSOR -> R.string.role_sensor - DeviceConfig.Role.TAK -> R.string.role_tak - DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden - DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found - DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker - DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late + DeviceConfig.Role.CLIENT -> R.string.role_client_desc + DeviceConfig.Role.CLIENT_MUTE -> R.string.role_client_mute_desc + DeviceConfig.Role.ROUTER -> R.string.role_router_desc + DeviceConfig.Role.ROUTER_CLIENT -> R.string.role_router_client_desc + DeviceConfig.Role.REPEATER -> R.string.role_repeater_desc + DeviceConfig.Role.TRACKER -> R.string.role_tracker_desc + DeviceConfig.Role.SENSOR -> R.string.role_sensor_desc + DeviceConfig.Role.TAK -> R.string.role_tak_desc + DeviceConfig.Role.CLIENT_HIDDEN -> R.string.role_client_hidden_desc + DeviceConfig.Role.LOST_AND_FOUND -> R.string.role_lost_and_found_desc + DeviceConfig.Role.TAK_TRACKER -> R.string.role_tak_tracker_desc + DeviceConfig.Role.ROUTER_LATE -> R.string.role_router_late_desc else -> R.string.unrecognized } -private val DeviceConfig.RebroadcastMode.stringRes: Int +private val DeviceConfig.RebroadcastMode.description: Int get() = when (this) { - DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all - DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding - DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only - DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only - DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none - DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only + DeviceConfig.RebroadcastMode.ALL -> R.string.rebroadcast_mode_all_desc + DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> R.string.rebroadcast_mode_all_skip_decoding_desc + DeviceConfig.RebroadcastMode.LOCAL_ONLY -> R.string.rebroadcast_mode_local_only_desc + DeviceConfig.RebroadcastMode.KNOWN_ONLY -> R.string.rebroadcast_mode_known_only_desc + DeviceConfig.RebroadcastMode.NONE -> R.string.rebroadcast_mode_none_desc + DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY -> R.string.rebroadcast_mode_core_portnums_only_desc else -> R.string.unrecognized } @@ -160,60 +160,37 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli } } LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.device_config)) } - + item { PreferenceCategory(text = stringResource(R.string.options)) } item { DropDownPreference( title = stringResource(R.string.role), enabled = enabled, selectedItem = deviceInput.role, onItemSelected = { selectedRole = it }, - summary = stringResource(id = deviceInput.role.stringRes), - ) - HorizontalDivider() - } - - item { - EditTextPreference( - title = stringResource(R.string.redefine_pin_button), - value = deviceInput.buttonGpio, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } }, + summary = stringResource(id = deviceInput.role.description), ) } - - item { - EditTextPreference( - title = stringResource(R.string.redefine_pin_buzzer), - value = deviceInput.buzzerGpio, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } }, - ) - } - + item { HorizontalDivider() } item { DropDownPreference( title = stringResource(R.string.rebroadcast_mode), enabled = enabled, selectedItem = deviceInput.rebroadcastMode, onItemSelected = { deviceInput = deviceInput.copy { rebroadcastMode = it } }, - summary = stringResource(id = deviceInput.rebroadcastMode.stringRes), + summary = stringResource(id = deviceInput.rebroadcastMode.description), ) - HorizontalDivider() } - + item { HorizontalDivider() } item { EditTextPreference( - title = stringResource(R.string.nodeinfo_broadcast_interval_seconds), + title = stringResource(R.string.nodeinfo_broadcast_interval), value = deviceInput.nodeInfoBroadcastSecs, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { deviceInput = deviceInput.copy { nodeInfoBroadcastSecs = it } }, ) } - + item { PreferenceCategory(text = stringResource(R.string.hardware)) } item { SwitchPreference( title = stringResource(R.string.double_tap_as_button_press), @@ -222,24 +199,35 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli enabled = enabled, onCheckedChange = { deviceInput = deviceInput.copy { doubleTapAsButtonPress = it } }, ) - HorizontalDivider() } - + item { HorizontalDivider() } item { SwitchPreference( - title = stringResource(R.string.disable_triple_click), - summary = stringResource(id = R.string.config_device_disableTripleClick_summary), - checked = deviceInput.disableTripleClick, + title = stringResource(R.string.triple_click_adhoc_ping), + summary = stringResource(id = R.string.config_device_tripleClickAsAdHocPing_summary), + checked = !deviceInput.disableTripleClick, enabled = enabled, - onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = it } }, + onCheckedChange = { deviceInput = deviceInput.copy { disableTripleClick = !it } }, ) - HorizontalDivider() } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.led_heartbeat), + summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary), + checked = !deviceInput.ledHeartbeatDisabled, + enabled = enabled, + onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = !it } }, + ) + } + item { HorizontalDivider() } + item { PreferenceCategory(text = stringResource(R.string.debug)) } item { EditTextPreference( - title = stringResource(R.string.posix_timezone), + title = stringResource(R.string.time_zone), value = deviceInput.tzdef, + summary = stringResource(id = R.string.config_device_tzdef_summary), maxSize = 64, // tzdef max_size:65 enabled = enabled, isError = false, @@ -250,17 +238,25 @@ fun DeviceConfigItemList(deviceConfig: DeviceConfig, enabled: Boolean, onSaveCli ) } + item { PreferenceCategory(text = stringResource(R.string.gpio)) } item { - SwitchPreference( - title = stringResource(R.string.disable_led_heartbeat), - summary = stringResource(id = R.string.config_device_ledHeartbeatDisabled_summary), - checked = deviceInput.ledHeartbeatDisabled, + EditTextPreference( + title = stringResource(R.string.button_gpio), + value = deviceInput.buttonGpio, enabled = enabled, - onCheckedChange = { deviceInput = deviceInput.copy { ledHeartbeatDisabled = it } }, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { deviceInput = deviceInput.copy { buttonGpio = it } }, + ) + } + item { + EditTextPreference( + title = stringResource(R.string.buzzer_gpio), + value = deviceInput.buzzerGpio, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { deviceInput = deviceInput.copy { buzzerGpio = it } }, ) - HorizontalDivider() } - item { PreferenceFooter( enabled = enabled && deviceInput != deviceConfig, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt index fdbd2b9f4..7deb941e0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/DisplayConfigItemList.kt @@ -69,52 +69,40 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave LazyColumn(modifier = Modifier.fillMaxSize()) { item { PreferenceCategory(text = stringResource(R.string.display_config)) } - - item { - EditTextPreference( - title = stringResource(R.string.screen_timeout_seconds), - value = displayInput.screenOnSecs, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } }, - ) - } - - item { HorizontalDivider() } - - item { - EditTextPreference( - title = stringResource(R.string.auto_screen_carousel_seconds), - value = displayInput.autoScreenCarouselSecs, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } }, - ) - } - item { SwitchPreference( - title = stringResource(R.string.compass_north_top), + title = stringResource(R.string.always_point_north), + summary = stringResource(id = R.string.config_display_compass_north_top_summary), checked = displayInput.compassNorthTop, enabled = enabled, onCheckedChange = { displayInput = displayInput.copy { compassNorthTop = it } }, ) } item { HorizontalDivider() } - item { SwitchPreference( - title = stringResource(R.string.flip_screen), - checked = displayInput.flipScreen, + title = stringResource(R.string.use_12h_format), + summary = stringResource(R.string.display_time_in_12h_format), enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } }, + checked = displayInput.use12HClock, + onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } }, + ) + } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.bold_heading), + summary = stringResource(id = R.string.config_display_heading_bold_summary), + checked = displayInput.headingBold, + enabled = enabled, + onCheckedChange = { displayInput = displayInput.copy { headingBold = it } }, ) } item { HorizontalDivider() } - item { DropDownPreference( title = stringResource(R.string.display_units), + summary = stringResource(id = R.string.config_display_units_summary), enabled = enabled, items = DisplayConfig.DisplayUnits.entries @@ -126,23 +114,54 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave } item { HorizontalDivider() } + item { PreferenceCategory(text = stringResource(R.string.advanced)) } item { - DropDownPreference( - title = stringResource(R.string.override_oled_auto_detect), + EditTextPreference( + title = stringResource(R.string.screen_on_for), + summary = stringResource(id = R.string.config_display_screen_on_secs_summary), + value = displayInput.screenOnSecs, enabled = enabled, - items = - DisplayConfig.OledType.entries - .filter { it != DisplayConfig.OledType.UNRECOGNIZED } - .map { it to it.name }, - selectedItem = displayInput.oled, - onItemSelected = { displayInput = displayInput.copy { oled = it } }, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { displayInput = displayInput.copy { screenOnSecs = it } }, ) } item { HorizontalDivider() } + item { + EditTextPreference( + title = stringResource(R.string.carousel_interval), + summary = stringResource(id = R.string.config_display_auto_screen_carousel_secs_summary), + value = displayInput.autoScreenCarouselSecs, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { displayInput = displayInput.copy { autoScreenCarouselSecs = it } }, + ) + } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.wake_on_tap_or_motion), + summary = stringResource(id = R.string.config_display_wake_on_tap_or_motion_summary), + checked = displayInput.wakeOnTapOrMotion, + enabled = enabled, + onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } }, + ) + } + item { HorizontalDivider() } + item { + SwitchPreference( + title = stringResource(R.string.flip_screen), + summary = stringResource(id = R.string.config_display_flip_screen_summary), + checked = displayInput.flipScreen, + enabled = enabled, + onCheckedChange = { displayInput = displayInput.copy { flipScreen = it } }, + ) + } + item { HorizontalDivider() } item { DropDownPreference( title = stringResource(R.string.display_mode), + summary = stringResource(id = R.string.config_display_displaymode_summary), enabled = enabled, items = DisplayConfig.DisplayMode.entries @@ -153,27 +172,20 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave ) } item { HorizontalDivider() } - item { - SwitchPreference( - title = stringResource(R.string.heading_bold), - checked = displayInput.headingBold, + DropDownPreference( + title = stringResource(R.string.oled_type), + summary = stringResource(id = R.string.config_display_oled_summary), enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { headingBold = it } }, + items = + DisplayConfig.OledType.entries + .filter { it != DisplayConfig.OledType.UNRECOGNIZED } + .map { it to it.name }, + selectedItem = displayInput.oled, + onItemSelected = { displayInput = displayInput.copy { oled = it } }, ) } item { HorizontalDivider() } - - item { - SwitchPreference( - title = stringResource(R.string.wake_screen_on_tap_or_motion), - checked = displayInput.wakeOnTapOrMotion, - enabled = enabled, - onCheckedChange = { displayInput = displayInput.copy { wakeOnTapOrMotion = it } }, - ) - } - item { HorizontalDivider() } - item { DropDownPreference( title = stringResource(R.string.compass_orientation), @@ -188,17 +200,6 @@ fun DisplayConfigItemList(displayConfig: DisplayConfig, enabled: Boolean, onSave } item { HorizontalDivider() } - item { - SwitchPreference( - title = stringResource(R.string.use_12h_format), - summary = stringResource(R.string.display_time_in_12h_format), - enabled = enabled, - checked = displayInput.use12HClock, - onCheckedChange = { displayInput = displayInput.copy { use12HClock = it } }, - ) - } - item { HorizontalDivider() } - item { PreferenceFooter( enabled = enabled && displayInput != displayConfig, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt index e2d4b80ac..49f0b1a0a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt @@ -83,8 +83,18 @@ fun LoRaConfigItemList( val primaryChannel by remember(loraInput) { mutableStateOf(Channel(primarySettings, loraInput)) } LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.lora_config)) } - + item { PreferenceCategory(text = stringResource(R.string.options)) } + item { + DropDownPreference( + title = stringResource(R.string.region_frequency_plan), + summary = stringResource(id = R.string.config_lora_region_summary), + enabled = enabled, + items = RegionInfo.entries.map { it.regionCode to it.description }, + selectedItem = loraInput.region, + onItemSelected = { loraInput = loraInput.copy { region = it } }, + ) + } + item { HorizontalDivider() } item { SwitchPreference( title = stringResource(R.string.use_modem_preset), @@ -99,6 +109,7 @@ fun LoRaConfigItemList( item { DropDownPreference( title = stringResource(R.string.modem_preset), + summary = stringResource(id = R.string.config_lora_modem_preset_summary), enabled = enabled && loraInput.usePreset, items = LoRaConfig.ModemPreset.entries @@ -140,37 +151,26 @@ fun LoRaConfigItemList( ) } } + item { PreferenceCategory(text = stringResource(R.string.advanced)) } item { - EditTextPreference( - title = stringResource(R.string.frequency_offset_mhz), - value = loraInput.frequencyOffset, + SwitchPreference( + title = stringResource(R.string.ignore_mqtt), + checked = loraInput.ignoreMqtt, enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { frequencyOffset = it } }, - ) - } - - item { - DropDownPreference( - title = stringResource(R.string.region_frequency_plan), - enabled = enabled, - items = RegionInfo.entries.map { it.regionCode to it.description }, - selectedItem = loraInput.region, - onItemSelected = { loraInput = loraInput.copy { region = it } }, + onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } }, ) } item { HorizontalDivider() } - item { - EditTextPreference( - title = stringResource(R.string.hop_limit), - value = loraInput.hopLimit, + SwitchPreference( + title = stringResource(R.string.ok_to_mqtt), + checked = loraInput.configOkToMqtt, enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { hopLimit = it } }, + onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } }, ) } + item { HorizontalDivider() } item { SwitchPreference( @@ -181,21 +181,23 @@ fun LoRaConfigItemList( ) } item { HorizontalDivider() } - item { - SignedIntegerEditTextPreference( - title = stringResource(R.string.tx_power_dbm), - value = loraInput.txPower, + EditTextPreference( + title = stringResource(R.string.hop_limit), + summary = stringResource(id = R.string.config_lora_hop_limit_summary), + value = loraInput.hopLimit, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { txPower = it } }, + onValueChanged = { loraInput = loraInput.copy { hopLimit = it } }, ) } + item { HorizontalDivider() } item { var isFocused by remember { mutableStateOf(false) } EditTextPreference( title = stringResource(R.string.frequency_slot), + summary = stringResource(id = R.string.config_lora_frequency_slot_summary), value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -207,17 +209,7 @@ fun LoRaConfigItemList( }, ) } - - item { - SwitchPreference( - title = stringResource(R.string.override_duty_cycle), - checked = loraInput.overrideDutyCycle, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { overrideDutyCycle = it } }, - ) - } item { HorizontalDivider() } - item { SwitchPreference( title = stringResource(R.string.sx126x_rx_boosted_gain), @@ -227,7 +219,6 @@ fun LoRaConfigItemList( ) } item { HorizontalDivider() } - item { var isFocused by remember { mutableStateOf(false) } EditTextPreference( @@ -244,6 +235,16 @@ fun LoRaConfigItemList( onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } }, ) } + item { HorizontalDivider() } + item { + SignedIntegerEditTextPreference( + title = stringResource(R.string.tx_power_dbm), + value = loraInput.txPower, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { loraInput = loraInput.copy { txPower = it } }, + ) + } if (hasPaFan) { item { @@ -257,26 +258,6 @@ fun LoRaConfigItemList( item { HorizontalDivider() } } - item { - SwitchPreference( - title = stringResource(R.string.ignore_mqtt), - checked = loraInput.ignoreMqtt, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { ignoreMqtt = it } }, - ) - } - item { HorizontalDivider() } - - item { - SwitchPreference( - title = stringResource(R.string.ok_to_mqtt), - checked = loraInput.configOkToMqtt, - enabled = enabled, - onCheckedChange = { loraInput = loraInput.copy { configOkToMqtt = it } }, - ) - } - item { HorizontalDivider() } - item { PreferenceFooter( enabled = enabled && loraInput != loraConfig, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt index 04e49c941..e539c7838 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/NetworkConfigItemList.kt @@ -131,63 +131,87 @@ fun NetworkConfigItemList( } LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.network_config)) } + if (hasWifi) { + item { PreferenceCategory(text = stringResource(R.string.wifi_config)) } + item { + SwitchPreference( + title = stringResource(R.string.wifi_enabled), + summary = stringResource(id = R.string.config_network_wifi_enabled_summary), + checked = networkInput.wifiEnabled, + enabled = enabled && hasWifi, + onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } }, + ) + HorizontalDivider() + } - item { - SwitchPreference( - title = stringResource(R.string.wifi_enabled), - checked = networkInput.wifiEnabled, - enabled = enabled && hasWifi, - onCheckedChange = { networkInput = networkInput.copy { wifiEnabled = it } }, - ) - HorizontalDivider() + item { + EditTextPreference( + title = stringResource(R.string.ssid), + value = networkInput.wifiSsid, + maxSize = 32, // wifi_ssid max_size:33 + enabled = enabled && hasWifi, + isError = false, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } }, + ) + } + + item { + EditPasswordPreference( + title = stringResource(R.string.password), + value = networkInput.wifiPsk, + maxSize = 64, // wifi_psk max_size:65 + enabled = enabled && hasWifi, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } }, + ) + } + + item { + Button( + onClick = { zxingScan() }, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp), + enabled = enabled && hasWifi, + ) { + Text(text = stringResource(R.string.wifi_qr_code_scan)) + } + } } - - item { - EditTextPreference( - title = stringResource(R.string.ssid), - value = networkInput.wifiSsid, - maxSize = 32, // wifi_ssid max_size:33 - enabled = enabled && hasWifi, - isError = false, - keyboardOptions = - KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { wifiSsid = it } }, - ) - } - - item { - EditPasswordPreference( - title = stringResource(R.string.psk), - value = networkInput.wifiPsk, - maxSize = 64, // wifi_psk max_size:65 - enabled = enabled && hasWifi, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { networkInput = networkInput.copy { wifiPsk = it } }, - ) - } - - item { - Button( - onClick = { zxingScan() }, - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp).height(48.dp), - enabled = enabled && hasWifi, - ) { - Text(text = stringResource(R.string.wifi_qr_code_scan)) + if (hasEthernet) { + item { PreferenceCategory(text = stringResource(R.string.ethernet_config)) } + item { + SwitchPreference( + title = stringResource(R.string.ethernet_enabled), + summary = stringResource(id = R.string.config_network_eth_enabled_summary), + checked = networkInput.ethEnabled, + enabled = enabled && hasEthernet, + onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } }, + ) + HorizontalDivider() } } - item { - SwitchPreference( - title = stringResource(R.string.ethernet_enabled), - checked = networkInput.ethEnabled, - enabled = enabled && hasEthernet, - onCheckedChange = { networkInput = networkInput.copy { ethEnabled = it } }, - ) - HorizontalDivider() + if (hasEthernet || hasWifi) { + item { PreferenceCategory(text = stringResource(R.string.udp_config)) } + + item { + SwitchPreference( + title = stringResource(R.string.udp_enabled), + summary = stringResource(id = R.string.config_network_udp_enabled_summary), + checked = networkInput.enabledProtocols == 1, + enabled = enabled, + onCheckedChange = { + networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 } + }, + ) + } + + item { HorizontalDivider() } } + item { PreferenceCategory(text = stringResource(R.string.advanced)) } item { EditTextPreference( title = stringResource(R.string.ntp_server), @@ -282,22 +306,7 @@ fun NetworkConfigItemList( ) } item { HorizontalDivider() } - if (hasEthernet || hasWifi) { - item { PreferenceCategory(text = stringResource(R.string.udp_config)) } - item { - SwitchPreference( - title = stringResource(R.string.mesh_via_udp_enabled), - checked = networkInput.enabledProtocols == 1, - enabled = enabled, - onCheckedChange = { - networkInput = networkInput.copy { if (it) enabledProtocols = 1 else enabledProtocols = 0 } - }, - ) - } - - item { HorizontalDivider() } - } item { PreferenceFooter( enabled = enabled && networkInput != networkConfig, diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt index d8d0eee65..813c8d3d4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PositionConfigItemList.kt @@ -144,11 +144,12 @@ fun PositionConfigItemList( } } LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.position_config)) } + item { PreferenceCategory(text = stringResource(R.string.position_packet)) } item { EditTextPreference( - title = stringResource(R.string.position_broadcast_interval_seconds), + title = stringResource(R.string.broadcast_interval), + summary = stringResource(id = R.string.config_position_broadcast_secs_summary), value = positionInput.positionBroadcastSecs, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -158,7 +159,7 @@ fun PositionConfigItemList( item { SwitchPreference( - title = stringResource(R.string.smart_position_enabled), + title = stringResource(R.string.smart_position), checked = positionInput.positionBroadcastSmartEnabled, enabled = enabled, onCheckedChange = { positionInput = positionInput.copy { positionBroadcastSmartEnabled = it } }, @@ -169,28 +170,30 @@ fun PositionConfigItemList( if (positionInput.positionBroadcastSmartEnabled) { item { EditTextPreference( - title = stringResource(R.string.smart_broadcast_minimum_distance_meters), - value = positionInput.broadcastSmartMinimumDistance, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } }, - ) - } - - item { - EditTextPreference( - title = stringResource(R.string.smart_broadcast_minimum_interval_seconds), + title = stringResource(R.string.minimum_interval), + summary = + stringResource(id = R.string.config_position_broadcast_smart_minimum_interval_secs_summary), value = positionInput.broadcastSmartMinimumIntervalSecs, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it } }, ) } + item { + EditTextPreference( + title = stringResource(R.string.minimum_distance), + summary = stringResource(id = R.string.config_position_broadcast_smart_minimum_distance_summary), + value = positionInput.broadcastSmartMinimumDistance, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } }, + ) + } } - + item { PreferenceCategory(text = stringResource(R.string.device_gps)) } item { SwitchPreference( - title = stringResource(R.string.use_fixed_position), + title = stringResource(R.string.fixed_position), checked = positionInput.fixedPosition, enabled = enabled, onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } }, @@ -227,7 +230,7 @@ fun PositionConfigItemList( } item { EditTextPreference( - title = stringResource(R.string.altitude_meters), + title = stringResource(R.string.altitude), value = locationInput.altitude, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -260,17 +263,19 @@ fun PositionConfigItemList( item { EditTextPreference( - title = stringResource(R.string.gps_update_interval_seconds), + title = stringResource(R.string.update_interval), + summary = stringResource(id = R.string.config_position_gps_update_interval_summary), value = positionInput.gpsUpdateInterval, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } }, ) } - + item { PreferenceCategory(text = stringResource(R.string.position_flags)) } item { BitwisePreference( title = stringResource(R.string.position_flags), + summary = stringResource(id = R.string.config_position_flags_summary), value = positionInput.positionFlags, enabled = enabled, items = @@ -283,10 +288,11 @@ fun PositionConfigItemList( ) } item { HorizontalDivider() } + item { PreferenceCategory(text = stringResource(R.string.advanced_device_gps)) } item { EditTextPreference( - title = stringResource(R.string.redefine_gps_rx_pin), + title = stringResource(R.string.gps_receive_gpio), value = positionInput.rxGpio, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -296,7 +302,7 @@ fun PositionConfigItemList( item { EditTextPreference( - title = stringResource(R.string.redefine_gps_tx_pin), + title = stringResource(R.string.gps_transmit_gpio), value = positionInput.txGpio, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -306,7 +312,7 @@ fun PositionConfigItemList( item { EditTextPreference( - title = stringResource(R.string.redefine_pin_gps_en), + title = stringResource(R.string.gps_en_gpio), value = positionInput.gpsEnGpio, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt index c05cf6722..10e2249bf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/PowerConfigItemList.kt @@ -65,6 +65,8 @@ fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) { fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit) { val focusManager = LocalFocusManager.current var powerInput by rememberSaveable { mutableStateOf(powerConfig) } + var shutdownOnPowerLoss by rememberSaveable { mutableStateOf(powerConfig.onBatteryShutdownAfterSecs > 0) } + var adcOverride by rememberSaveable { mutableStateOf(powerConfig.adcMultiplierOverride > 0f) } LazyColumn(modifier = Modifier.fillMaxSize()) { item { PreferenceCategory(text = stringResource(R.string.power_config)) } @@ -72,6 +74,7 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke item { SwitchPreference( title = stringResource(R.string.enable_power_saving_mode), + summary = stringResource(id = R.string.config_power_is_power_saving_summary), checked = powerInput.isPowerSaving, enabled = enabled, onCheckedChange = { powerInput = powerInput.copy { isPowerSaving = it } }, @@ -80,25 +83,57 @@ fun PowerConfigItemList(powerConfig: PowerConfig, enabled: Boolean, onSaveClicke item { HorizontalDivider() } item { - EditTextPreference( - title = stringResource(R.string.shutdown_on_battery_delay_seconds), - value = powerInput.onBatteryShutdownAfterSecs, + SwitchPreference( + title = stringResource(R.string.shutdown_on_power_loss), + checked = shutdownOnPowerLoss, enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } }, + onCheckedChange = { + shutdownOnPowerLoss = it + if (!it) powerInput = powerInput.copy { onBatteryShutdownAfterSecs = 0 } + }, ) } + if (shutdownOnPowerLoss) { + item { + EditTextPreference( + title = stringResource(R.string.shutdown_on_battery_delay_seconds), + value = powerInput.onBatteryShutdownAfterSecs, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { powerInput = powerInput.copy { onBatteryShutdownAfterSecs = it } }, + ) + } + } + + item { HorizontalDivider() } + item { - EditTextPreference( - title = stringResource(R.string.adc_multiplier_override_ratio), - value = powerInput.adcMultiplierOverride, + SwitchPreference( + title = stringResource(R.string.adc_multiplier_override), + checked = adcOverride, enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } }, + onCheckedChange = { + adcOverride = it + if (!it) powerInput = powerInput.copy { adcMultiplierOverride = 0f } + }, ) } + if (adcOverride) { + item { + EditTextPreference( + title = stringResource(R.string.adc_multiplier_override_ratio), + value = powerInput.adcMultiplierOverride, + enabled = enabled, + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { powerInput = powerInput.copy { adcMultiplierOverride = it } }, + ) + } + } + + item { HorizontalDivider() } + item { EditTextPreference( title = stringResource(R.string.wait_for_bluetooth_duration_seconds), diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt index 1f4b27193..7e9981eb2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/SecurityConfigItemList.kt @@ -154,18 +154,19 @@ fun SecurityConfigItemList( } LazyColumn(modifier = Modifier.fillMaxSize()) { - item { PreferenceCategory(text = stringResource(R.string.security_config)) } + item { PreferenceCategory(text = stringResource(R.string.direct_message_key)) } item { EditBase64Preference( title = stringResource(R.string.public_key), + summary = stringResource(id = R.string.config_security_public_key), value = publicKey, enabled = enabled, readOnly = true, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size() == 32) { - securityInput = securityInput.copy { publicKey = it } + securityInput = securityInput.copy { this.publicKey = it } } }, trailingIcon = { CopyIconButton(valueToCopy = securityInput.publicKey.encodeToString()) }, @@ -175,6 +176,7 @@ fun SecurityConfigItemList( item { EditBase64Preference( title = stringResource(R.string.private_key), + summary = stringResource(id = R.string.config_security_private_key), value = securityInput.privateKey, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), @@ -206,10 +208,11 @@ fun SecurityConfigItemList( onClick = { showEditSecurityConfigDialog = true }, ) } - + item { PreferenceCategory(text = stringResource(R.string.admin_keys)) } item { EditListPreference( title = stringResource(R.string.admin_key), + summary = stringResource(id = R.string.config_security_admin_key), list = securityInput.adminKeyList, maxCount = 3, enabled = enabled, @@ -223,20 +226,11 @@ fun SecurityConfigItemList( }, ) } - - item { - SwitchPreference( - title = stringResource(R.string.managed_mode), - checked = securityInput.isManaged, - enabled = enabled && securityInput.adminKeyCount > 0, - onCheckedChange = { securityInput = securityInput.copy { isManaged = it } }, - ) - } - item { HorizontalDivider() } - + item { PreferenceCategory(text = stringResource(R.string.logs)) } item { SwitchPreference( title = stringResource(R.string.serial_console), + summary = stringResource(id = R.string.config_security_serial_enabled), checked = securityInput.serialEnabled, enabled = enabled, onCheckedChange = { securityInput = securityInput.copy { serialEnabled = it } }, @@ -247,12 +241,24 @@ fun SecurityConfigItemList( item { SwitchPreference( title = stringResource(R.string.debug_log_api_enabled), + summary = stringResource(id = R.string.config_security_debug_log_api_enabled), checked = securityInput.debugLogApiEnabled, enabled = enabled, onCheckedChange = { securityInput = securityInput.copy { debugLogApiEnabled = it } }, ) } item { HorizontalDivider() } + item { PreferenceCategory(text = stringResource(R.string.administration)) } + item { + SwitchPreference( + title = stringResource(R.string.managed_mode), + summary = stringResource(id = R.string.config_security_is_managed), + checked = securityInput.isManaged, + enabled = enabled && securityInput.adminKeyCount > 0, + onCheckedChange = { securityInput = securityInput.copy { isManaged = it } }, + ) + } + item { HorizontalDivider() } item { SwitchPreference( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 006942df7..3df9c88d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,31 +66,100 @@ Bad session key Public Key unauthorized - App connected or standalone messaging device. - Device that does not forward packets from other devices. - Infrastructure node for extending network coverage by relaying messages. Visible in nodes list. - Combination of both ROUTER and CLIENT. Not for mobile devices. - Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list. - Broadcasts GPS position packets as priority. - Broadcasts telemetry packets as priority. - Optimized for ATAK system communication, reduces routine broadcasts. - Device that only broadcasts as needed for stealth or power savings. - Broadcasts location as message to default channel regularly for to assist with device recovery. - Enables automatic TAK PLI broadcasts and reduces routine broadcasts. - Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in nodes list. + Client + App connected or standalone messaging device. + Client Mute + Device that does not forward packets from other devices. + Router + Infrastructure node for extending network coverage by relaying messages. Visible in nodes list. + Router Client + Combination of both ROUTER and CLIENT. Not for mobile devices. + Repeater + Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list. + Tracker + Broadcasts GPS position packets as priority. + Sensor + Broadcasts telemetry packets as priority. + TAK + Optimized for ATAK system communication, reduces routine broadcasts. + Client Hidden + Device that only broadcasts as needed for stealth or power savings. + Lost and Found + Broadcasts location as message to default channel regularly for to assist with device recovery. + TAK Tracker + Enables automatic TAK PLI broadcasts and reduces routine broadcasts. + Router Late + Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in nodes list. - Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters. - Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior. - Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels. - Ignores observed messages from foreign meshes like LOCAL ONLY, but takes it step further by also ignoring messages from nodes not already in the node\'s known list. - Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. - Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. + All + Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters. + All Skip Decoding + Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior. + Local Only + Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels. + Known Only + Ignores observed messages from foreign meshes like LOCAL ONLY, but takes it step further by also ignoring messages from nodes not already in the node\'s known list. + None + Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. + Core Portnums Only + Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. Treat double tap on supported accelerometers as a user button press. - Disables the triple-press of user button to enable or disable GPS. - Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDs, the charger and GPS LEDs are not controllable. + Send a position on the primary channel when the user button is triple clicked. + Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDs, the charger and GPS LEDs are not controllable. + Time zone for dates on the device screen and log. Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. Not available on a channel with default key and name. - Public Key + + How long the screen remains on after the user button is pressed or messages are received. + Automatically toggles to the next page on the screen like a carousel, based the specified interval. + The compass heading on the screen outside of the circle will always point north. + Flip screen vertically. + Units displayed on the device screen. + Override automatic OLED screen detection. + Override default screen layout. + Bold the heading text on the screen. + Requires that there be an accelerometer on your device. + + The region where you will be using your radios. + Available modem presets, default is Long Fast. + Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs. + Your node’s operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name. + + Enabling WiFi will disable the bluetooth connection to the app. + Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices. + Enable broadcasting packets via UDP over the local network. + + The maximum interval that can elapse without a node broadcasting a position. + The fastest that position updates will be sent if the minimum distance has been satisfied. + The minimum distance change in meters to be considered for a smart position broadcast. + How often should we try to get a GPS position. + Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss. + + Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don\'t use this setting if you want to use your device with the phone apps or are using a device without a user button. + + Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key. + Used to create a shared key with a remote device. + The public key authorized to send admin messages to this node. + Device is managed by a mesh administrator, the user is unable to access any of the device settings. + Serial Console over the Stream API. + Output live debug logging over serial, view and export position-redacted device logs over Bluetooth. + + + Position Packet + Broadcast Interval + Smart Position + Minimum Interval + Minimum Distance + Device GPS + Fixed Position + Altitude + Update Interval + Advanced Device GPS + GPS Receive GPIO + GPS Transmit GPIO + GPS EN GPIO + GPIO + Debug MSL ChUtil %.1f%% AirUtilTX %.1f%% @@ -333,7 +402,8 @@ Low battery: %s Low battery notifications (favorite nodes) Barometric Pressure - Mesh via UDP enabled + Enabled + UDP Broadcast UDP Config Last heard: %2$s
Last position: %3$s
Battery: %4$s]]>
Toggle my position @@ -411,27 +481,27 @@ GPIO pin to monitor Detection trigger type Use INPUT_PULLUP mode - Device Config - Role - Redefine PIN_BUTTON - Redefine PIN_BUZZER - Rebroadcast mode - NodeInfo broadcast interval (seconds) - Double tap as button press - Disable triple-click - POSIX Timezone - Disable LED heartbeat - Display Config - Screen timeout (seconds) - GPS coordinates format - Auto screen carousel (seconds) + Device + Device Role + Button GPIO + Buzzer GPIO + Rebroadcast Mode + Node Info Broadcast Interval + Double Tap as Button + Triple Click Ad Hoc Ping + Time Zone + LED Heartbeat + Display + Screen on for + Carousel interval Compass north top Flip screen Display units - Override OLED auto-detect + OLED type Display mode - Heading bold - Wake screen on tap or motion + Always point north + Bold Heading + Wake on tap or motion Compass orientation External Notification Config External notification enabled @@ -452,25 +522,27 @@ Nag timeout (seconds) Ringtone Use I2S as buzzer - LoRa Config - Use modem preset - Modem Preset + LoRa + Options + Advanced + Use Preset + Presets Bandwidth - Spread factor - Coding rate + Spread Factor + Coding Rate Frequency offset (MHz) - Region (frequency plan) - Hop limit - TX enabled - TX power (dBm) - Frequency slot + Region + Number of hops + Transmit Enabled + Transmit Power + Frequency Slot Override Duty Cycle Ignore incoming - SX126X RX boosted gain - Override frequency (MHz) + RX Boosted Gain + Frequency Override PA fan disabled Ignore MQTT - OK to MQTT + Ok to MQTT MQTT Config MQTT enabled Address @@ -487,10 +559,13 @@ Neighbor Info enabled Update interval (seconds) Transmit over LoRa - Network Config + Network + WiFi Options + Enabled WiFi enabled SSID PSK + Ethernet Options Ethernet enabled NTP server rsyslog server @@ -502,7 +577,7 @@ Paxcounter enabled WiFi RSSI threshold (defaults to -80) BLE RSSI threshold (defaults to -80) - Position Config + Position Position broadcast interval (seconds) Smart position enabled Smart broadcast minimum distance (meters) @@ -520,7 +595,9 @@ Position flags Power Config Enable power saving mode + Shutdown on power loss Shutdown on battery delay (seconds) + ADC multiplier override ADC multiplier override ratio Wait for Bluetooth duration (seconds) Super deep sleep duration (seconds) @@ -535,7 +612,9 @@ Remote Hardware enabled Allow undefined pin access Available pins - Security Config + Security + Direct Message Key + Admin Keys Public Key Private Key Admin Key @@ -818,4 +897,5 @@ Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device. Icon Meanings Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required. + Device configuration