feat(settings): replace interval inputs with dropdowns (#3352)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-10-09 14:03:06 -05:00 committed by GitHub
parent e5a28d6942
commit c6be5be72f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2343 additions and 2028 deletions

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
@ -32,7 +33,6 @@ 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.protobuf.ProtocolMessageEnum
@ -84,49 +84,45 @@ fun <T> DropDownPreference(
emptyList()
}
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = "",
onValueChange = {},
prefix = { Text(title) },
suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors =
ExposedDropdownMenuDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
enabled = enabled,
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
onItemSelected(selectionOption.first)
expanded = false
},
)
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (enabled) {
expanded = !expanded
}
},
) {
OutlinedTextField(
label = { Text(text = title) },
modifier =
Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
onValueChange = {},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
enabled = enabled,
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items
.filterNot { it.first in deprecatedItems }
.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption.second) },
onClick = {
onItemSelected(selectionOption.first)
expanded = false
},
)
}
}
}
}
}

View file

@ -26,10 +26,8 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -40,12 +38,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
@ -211,15 +207,11 @@ fun EditTextPreference(
) {
var isFocused by remember { mutableStateOf(false) }
Column(modifier = modifier) {
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().onFocusEvent { onFocusChanged(it) },
value = value,
singleLine = true,
modifier =
Modifier.fillMaxWidth().onFocusEvent {
isFocused = it.isFocused
onFocusChanged(it)
},
enabled = enabled,
isError = isError,
onValueChange = {
@ -231,7 +223,7 @@ fun EditTextPreference(
onValueChanged(it)
}
},
prefix = { Text(title) },
label = { Text(title) },
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
@ -249,14 +241,6 @@ fun EditTextPreference(
} else {
null
},
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.End),
)
if (summary != null) {
Text(

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ListItem
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
import kotlin.math.roundToInt
@Composable
fun <T> SliderPreference(
title: String,
enabled: Boolean,
items: List<Pair<T, String>>,
selectedValue: T,
onValueChange: (T) -> Unit,
modifier: Modifier = Modifier,
summary: String? = null,
) {
if (items.isEmpty()) return
val selectedIndex = items.indexOfFirst { it.first == selectedValue }.toFloat()
val valueRange = 0f..(items.size - 1).toFloat()
val steps = (items.size - 2).coerceAtLeast(0)
ListItem(
modifier = modifier,
headlineContent = {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End,
text = items.firstOrNull { it.first == selectedValue }?.second ?: items.first().second,
)
},
overlineContent = { Text(text = title) },
supportingContent = {
Column {
summary?.let { Text(text = it, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) }
Slider(
value = selectedIndex.coerceIn(valueRange),
onValueChange = {
val index = it.roundToInt()
if (index in items.indices) {
onValueChange(items[index].first)
}
},
valueRange = valueRange,
steps = steps,
enabled = enabled,
)
}
},
)
}
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferencePreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
title = "Slider",
summary = "Select a value",
enabled = true,
items = items,
selectedValue = 3L,
onValueChange = {},
)
}
}
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
private fun SliderPreferenceDisabledPreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
title = "Slider",
summary = "Select a value",
enabled = false,
items = items,
selectedValue = 3L,
onValueChange = {},
)
}
}