feat(settings): align config screens copy and order with iOS (#3144)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-09-19 17:58:49 -05:00 committed by GitHub
parent 8fb41aab74
commit 00ee0db78a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 899 additions and 724 deletions

View file

@ -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<String>()) }
var filterTexts by
androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf<String>()) }
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<String>()) }
var filterTexts by
androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(listOf<String>()) }
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()
}
}
}

View file

@ -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<String>()) }
var customFilterText by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf("") }
var filterTexts by androidx.compose.runtime.remember { mutableStateOf(listOf<String>()) }
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<String>()) }
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<String>()) }
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()
}
}
}

View file

@ -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<ConfigRoute> = entries.filter {
private fun filterExcludedFrom(metadata: DeviceMetadata?): List<ConfigRoute> = 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<ConfigRoute> =
filterExcludedFrom(metadata) - radioConfigRoutes
}
}

View file

@ -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<Pair<Int, String>>,
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 = {},
)
}

View file

@ -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 <T : Enum<T>> 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 <T : Enum<T>> DropDownPreference(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DropDownPreference(
title: String,
@ -69,7 +69,7 @@ fun <T> DropDownPreference(
modifier: Modifier = Modifier,
summary: String? = null,
) {
var dropDownExpanded by remember { mutableStateOf(value = false) }
var expanded by remember { mutableStateOf(false) }
val deprecatedItems: List<T> = remember {
if (selectedItem is ProtocolMessageEnum) {
@ -77,58 +77,46 @@ fun <T> 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<T> ?: emptyList() // Safe cast to List<T> or return emptyList if cast fails
enum?.filter { entries -> descriptor.values.any { it.name == entries.name && it.options.deprecated } }
as? List<T> ?: emptyList() // Safe cast to List<T> 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 = {},
)
}

View file

@ -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 {},

View file

@ -47,6 +47,7 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.remoteHardwarePin
import com.google.protobuf.ByteString
@Suppress("LongMethod")
@Composable
inline fun <reified T> EditListPreference(
title: String,
@ -56,12 +57,21 @@ inline fun <reified T> EditListPreference(
keyboardActions: KeyboardActions,
crossinline onValuesChanged: (List<T>) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
val focusManager = LocalFocusManager.current
val listState = remember(list) { mutableStateListOf<T>().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 <reified T> EditListPreference(
}
}
// handle lora.ignoreIncoming: List<Int>
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<ByteString>
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<RemoteHardwarePin>
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,

View file

@ -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 = {},
)
}
}

View file

@ -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)
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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() }),

View file

@ -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),

View file

@ -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(

View file

@ -66,31 +66,100 @@
<string name="routing_error_admin_bad_session_key">Bad session key</string>
<string name="routing_error_admin_public_key_unauthorized">Public Key unauthorized</string>
<string name="role_client">App connected or standalone messaging device.</string>
<string name="role_client_mute">Device that does not forward packets from other devices.</string>
<string name="role_router">Infrastructure node for extending network coverage by relaying messages. Visible in nodes list.</string>
<string name="role_router_client">Combination of both ROUTER and CLIENT. Not for mobile devices.</string>
<string name="role_repeater">Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list.</string>
<string name="role_tracker">Broadcasts GPS position packets as priority.</string>
<string name="role_sensor">Broadcasts telemetry packets as priority.</string>
<string name="role_tak">Optimized for ATAK system communication, reduces routine broadcasts.</string>
<string name="role_client_hidden">Device that only broadcasts as needed for stealth or power savings.</string>
<string name="role_lost_and_found">Broadcasts location as message to default channel regularly for to assist with device recovery.</string>
<string name="role_tak_tracker">Enables automatic TAK PLI broadcasts and reduces routine broadcasts.</string>
<string name="role_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.</string>
<string name="role_client">Client</string>
<string name="role_client_desc">App connected or standalone messaging device.</string>
<string name="role_client_mute">Client Mute</string>
<string name="role_client_mute_desc">Device that does not forward packets from other devices.</string>
<string name="role_router">Router</string>
<string name="role_router_desc">Infrastructure node for extending network coverage by relaying messages. Visible in nodes list.</string>
<string name="role_router_client">Router Client</string>
<string name="role_router_client_desc">Combination of both ROUTER and CLIENT. Not for mobile devices.</string>
<string name="role_repeater">Repeater</string>
<string name="role_repeater_desc">Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list.</string>
<string name="role_tracker">Tracker</string>
<string name="role_tracker_desc">Broadcasts GPS position packets as priority.</string>
<string name="role_sensor">Sensor</string>
<string name="role_sensor_desc">Broadcasts telemetry packets as priority.</string>
<string name="role_tak">TAK</string>
<string name="role_tak_desc">Optimized for ATAK system communication, reduces routine broadcasts.</string>
<string name="role_client_hidden">Client Hidden</string>
<string name="role_client_hidden_desc">Device that only broadcasts as needed for stealth or power savings.</string>
<string name="role_lost_and_found">Lost and Found</string>
<string name="role_lost_and_found_desc">Broadcasts location as message to default channel regularly for to assist with device recovery.</string>
<string name="role_tak_tracker">TAK Tracker</string>
<string name="role_tak_tracker_desc">Enables automatic TAK PLI broadcasts and reduces routine broadcasts.</string>
<string name="role_router_late">Router Late</string>
<string name="role_router_late_desc">Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in nodes list.</string>
<string name="rebroadcast_mode_all">Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters.</string>
<string name="rebroadcast_mode_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.</string>
<string name="rebroadcast_mode_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.</string>
<string name="rebroadcast_mode_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.</string>
<string name="rebroadcast_mode_none">Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.</string>
<string name="rebroadcast_mode_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.</string>
<string name="rebroadcast_mode_all">All</string>
<string name="rebroadcast_mode_all_desc">Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters.</string>
<string name="rebroadcast_mode_all_skip_decoding">All Skip Decoding</string>
<string name="rebroadcast_mode_all_skip_decoding_desc">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.</string>
<string name="rebroadcast_mode_local_only">Local Only</string>
<string name="rebroadcast_mode_local_only_desc">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.</string>
<string name="rebroadcast_mode_known_only">Known Only</string>
<string name="rebroadcast_mode_known_only_desc">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.</string>
<string name="rebroadcast_mode_none">None</string>
<string name="rebroadcast_mode_none_desc">Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.</string>
<string name="rebroadcast_mode_core_portnums_only">Core Portnums Only</string>
<string name="rebroadcast_mode_core_portnums_only_desc">Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing.</string>
<string name="config_device_doubleTapAsButtonPress_summary">Treat double tap on supported accelerometers as a user button press.</string>
<string name="config_device_disableTripleClick_summary">Disables the triple-press of user button to enable or disable GPS.</string>
<string name="config_device_ledHeartbeatDisabled_summary">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.</string>
<string name="config_device_tripleClickAsAdHocPing_summary">Send a position on the primary channel when the user button is triple clicked.</string>
<string name="config_device_ledHeartbeatEnabled_summary">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.</string>
<string name="config_device_tzdef_summary">Time zone for dates on the device screen and log.</string>
<string name="config_device_transmitOverLora_summary">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.</string>
<string name="config_security_public_key">Public Key</string>
<string name="config_display_screen_on_secs_summary">How long the screen remains on after the user button is pressed or messages are received.</string>
<string name="config_display_auto_screen_carousel_secs_summary">Automatically toggles to the next page on the screen like a carousel, based the specified interval.</string>
<string name="config_display_compass_north_top_summary">The compass heading on the screen outside of the circle will always point north.</string>
<string name="config_display_flip_screen_summary">Flip screen vertically.</string>
<string name="config_display_units_summary">Units displayed on the device screen.</string>
<string name="config_display_oled_summary">Override automatic OLED screen detection.</string>
<string name="config_display_displaymode_summary">Override default screen layout.</string>
<string name="config_display_heading_bold_summary">Bold the heading text on the screen.</string>
<string name="config_display_wake_on_tap_or_motion_summary">Requires that there be an accelerometer on your device.</string>
<string name="config_lora_region_summary">The region where you will be using your radios.</string>
<string name="config_lora_modem_preset_summary">Available modem presets, default is Long Fast.</string>
<string name="config_lora_hop_limit_summary">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.</string>
<string name="config_lora_frequency_slot_summary">Your nodes 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.</string>
<string name="config_network_wifi_enabled_summary">Enabling WiFi will disable the bluetooth connection to the app.</string>
<string name="config_network_eth_enabled_summary">Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.</string>
<string name="config_network_udp_enabled_summary">Enable broadcasting packets via UDP over the local network.</string>
<string name="config_position_broadcast_secs_summary">The maximum interval that can elapse without a node broadcasting a position.</string>
<string name="config_position_broadcast_smart_minimum_interval_secs_summary">The fastest that position updates will be sent if the minimum distance has been satisfied.</string>
<string name="config_position_broadcast_smart_minimum_distance_summary">The minimum distance change in meters to be considered for a smart position broadcast.</string>
<string name="config_position_gps_update_interval_summary">How often should we try to get a GPS position.</string>
<string name="config_position_flags_summary">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.</string>
<string name="config_power_is_power_saving_summary">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.</string>
<string name="config_security_public_key">Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key.</string>
<string name="config_security_private_key">Used to create a shared key with a remote device.</string>
<string name="config_security_admin_key">The public key authorized to send admin messages to this node.</string>
<string name="config_security_is_managed">Device is managed by a mesh administrator, the user is unable to access any of the device settings.</string>
<string name="config_security_serial_enabled">Serial Console over the Stream API.</string>
<string name="config_security_debug_log_api_enabled">Output live debug logging over serial, view and export position-redacted device logs over Bluetooth.</string>
<!-- Position Config -->
<string name="position_packet">Position Packet</string>
<string name="broadcast_interval">Broadcast Interval</string>
<string name="smart_position">Smart Position</string>
<string name="minimum_interval">Minimum Interval</string>
<string name="minimum_distance">Minimum Distance</string>
<string name="device_gps">Device GPS</string>
<string name="fixed_position">Fixed Position</string>
<string name="altitude">Altitude</string>
<string name="update_interval">Update Interval</string>
<string name="advanced_device_gps">Advanced Device GPS</string>
<string name="gps_receive_gpio">GPS Receive GPIO</string>
<string name="gps_transmit_gpio">GPS Transmit GPIO</string>
<string name="gps_en_gpio">GPS EN GPIO</string>
<string name="gpio">GPIO</string>
<string name="debug">Debug</string>
<string name="elevation_suffix" translatable="false">MSL</string>
<string name="channel_air_util" translatable="false">ChUtil %.1f%% AirUtilTX %.1f%%</string>
@ -333,7 +402,8 @@
<string name="low_battery_title">Low battery: %s</string>
<string name="meshtastic_low_battery_temporary_remote_notifications">Low battery notifications (favorite nodes)</string>
<string name="baro_pressure">Barometric Pressure</string>
<string name="mesh_via_udp_enabled">Mesh via UDP enabled</string>
<string name="udp_enabled">Enabled</string>
<string name="udp_broadcast">UDP Broadcast</string>
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
@ -411,27 +481,27 @@
<string name="gpio_pin_to_monitor">GPIO pin to monitor</string>
<string name="detection_trigger_type">Detection trigger type</string>
<string name="use_input_pullup_mode">Use INPUT_PULLUP mode</string>
<string name="device_config">Device Config</string>
<string name="role">Role</string>
<string name="redefine_pin_button">Redefine PIN_BUTTON</string>
<string name="redefine_pin_buzzer">Redefine PIN_BUZZER</string>
<string name="rebroadcast_mode">Rebroadcast mode</string>
<string name="nodeinfo_broadcast_interval_seconds">NodeInfo broadcast interval (seconds)</string>
<string name="double_tap_as_button_press">Double tap as button press</string>
<string name="disable_triple_click">Disable triple-click</string>
<string name="posix_timezone">POSIX Timezone</string>
<string name="disable_led_heartbeat">Disable LED heartbeat</string>
<string name="display_config">Display Config</string>
<string name="screen_timeout_seconds">Screen timeout (seconds)</string>
<string name="gps_coordinates_format">GPS coordinates format</string>
<string name="auto_screen_carousel_seconds">Auto screen carousel (seconds)</string>
<string name="device_config">Device</string>
<string name="role">Device Role</string>
<string name="button_gpio">Button GPIO</string>
<string name="buzzer_gpio">Buzzer GPIO</string>
<string name="rebroadcast_mode">Rebroadcast Mode</string>
<string name="nodeinfo_broadcast_interval">Node Info Broadcast Interval</string>
<string name="double_tap_as_button_press">Double Tap as Button</string>
<string name="triple_click_adhoc_ping">Triple Click Ad Hoc Ping</string>
<string name="time_zone">Time Zone</string>
<string name="led_heartbeat">LED Heartbeat</string>
<string name="display_config">Display</string>
<string name="screen_on_for">Screen on for</string>
<string name="carousel_interval">Carousel interval</string>
<string name="compass_north_top">Compass north top</string>
<string name="flip_screen">Flip screen</string>
<string name="display_units">Display units</string>
<string name="override_oled_auto_detect">Override OLED auto-detect</string>
<string name="oled_type">OLED type</string>
<string name="display_mode">Display mode</string>
<string name="heading_bold">Heading bold</string>
<string name="wake_screen_on_tap_or_motion">Wake screen on tap or motion</string>
<string name="always_point_north">Always point north</string>
<string name="bold_heading">Bold Heading</string>
<string name="wake_on_tap_or_motion">Wake on tap or motion</string>
<string name="compass_orientation">Compass orientation</string>
<string name="external_notification_config">External Notification Config</string>
<string name="external_notification_enabled">External notification enabled</string>
@ -452,25 +522,27 @@
<string name="nag_timeout_seconds">Nag timeout (seconds)</string>
<string name="ringtone">Ringtone</string>
<string name="use_i2s_as_buzzer">Use I2S as buzzer</string>
<string name="lora_config">LoRa Config</string>
<string name="use_modem_preset">Use modem preset</string>
<string name="modem_preset">Modem Preset</string>
<string name="lora_config">LoRa</string>
<string name="options">Options</string>
<string name="advanced">Advanced</string>
<string name="use_modem_preset">Use Preset</string>
<string name="modem_preset">Presets</string>
<string name="bandwidth">Bandwidth</string>
<string name="spread_factor">Spread factor</string>
<string name="coding_rate">Coding rate</string>
<string name="spread_factor">Spread Factor</string>
<string name="coding_rate">Coding Rate</string>
<string name="frequency_offset_mhz">Frequency offset (MHz)</string>
<string name="region_frequency_plan">Region (frequency plan)</string>
<string name="hop_limit">Hop limit</string>
<string name="tx_enabled">TX enabled</string>
<string name="tx_power_dbm">TX power (dBm)</string>
<string name="frequency_slot">Frequency slot</string>
<string name="region_frequency_plan">Region</string>
<string name="hop_limit">Number of hops</string>
<string name="tx_enabled">Transmit Enabled</string>
<string name="tx_power_dbm">Transmit Power</string>
<string name="frequency_slot">Frequency Slot</string>
<string name="override_duty_cycle">Override Duty Cycle</string>
<string name="ignore_incoming">Ignore incoming</string>
<string name="sx126x_rx_boosted_gain">SX126X RX boosted gain</string>
<string name="override_frequency_mhz">Override frequency (MHz)</string>
<string name="sx126x_rx_boosted_gain">RX Boosted Gain</string>
<string name="override_frequency_mhz">Frequency Override</string>
<string name="pa_fan_disabled">PA fan disabled</string>
<string name="ignore_mqtt">Ignore MQTT</string>
<string name="ok_to_mqtt">OK to MQTT</string>
<string name="ok_to_mqtt">Ok to MQTT</string>
<string name="mqtt_config">MQTT Config</string>
<string name="mqtt_enabled">MQTT enabled</string>
<string name="address">Address</string>
@ -487,10 +559,13 @@
<string name="neighbor_info_enabled">Neighbor Info enabled</string>
<string name="update_interval_seconds">Update interval (seconds)</string>
<string name="transmit_over_lora">Transmit over LoRa</string>
<string name="network_config">Network Config</string>
<string name="network_config">Network</string>
<string name="wifi_config">WiFi Options</string>
<string name="enabled">Enabled</string>
<string name="wifi_enabled">WiFi enabled</string>
<string name="ssid">SSID</string>
<string name="psk">PSK</string>
<string name="ethernet_config">Ethernet Options</string>
<string name="ethernet_enabled">Ethernet enabled</string>
<string name="ntp_server">NTP server</string>
<string name="rsyslog_server">rsyslog server</string>
@ -502,7 +577,7 @@
<string name="paxcounter_enabled">Paxcounter enabled</string>
<string name="wifi_rssi_threshold_defaults_to_80">WiFi RSSI threshold (defaults to -80)</string>
<string name="ble_rssi_threshold_defaults_to_80">BLE RSSI threshold (defaults to -80)</string>
<string name="position_config">Position Config</string>
<string name="position_config">Position</string>
<string name="position_broadcast_interval_seconds">Position broadcast interval (seconds)</string>
<string name="smart_position_enabled">Smart position enabled</string>
<string name="smart_broadcast_minimum_distance_meters">Smart broadcast minimum distance (meters)</string>
@ -520,7 +595,9 @@
<string name="position_flags">Position flags</string>
<string name="power_config">Power Config</string>
<string name="enable_power_saving_mode">Enable power saving mode</string>
<string name="shutdown_on_power_loss">Shutdown on power loss</string>
<string name="shutdown_on_battery_delay_seconds">Shutdown on battery delay (seconds)</string>
<string name="adc_multiplier_override">ADC multiplier override</string>
<string name="adc_multiplier_override_ratio">ADC multiplier override ratio</string>
<string name="wait_for_bluetooth_duration_seconds">Wait for Bluetooth duration (seconds)</string>
<string name="super_deep_sleep_duration_seconds">Super deep sleep duration (seconds)</string>
@ -535,7 +612,9 @@
<string name="remote_hardware_enabled">Remote Hardware enabled</string>
<string name="allow_undefined_pin_access">Allow undefined pin access</string>
<string name="available_pins">Available pins</string>
<string name="security_config">Security Config</string>
<string name="security_config">Security</string>
<string name="direct_message_key">Direct Message Key</string>
<string name="admin_keys">Admin Keys</string>
<string name="public_key">Public Key</string>
<string name="private_key">Private Key</string>
<string name="admin_key">Admin Key</string>
@ -818,4 +897,5 @@
<string name="downlink_feature_description">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.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
<string name="device_configuration">Device configuration</string>
</resources>