Switch back to older text input (#459)

Revert some of the changes to input text fields from #327. This PR
switches back to using the `value/onValueChange` variants for single
line input.

Fixes #413
This commit is contained in:
damontecres 2025-12-13 21:03:41 -05:00 committed by GitHub
parent a386028b6b
commit 43703436c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 284 additions and 52 deletions

View file

@ -5,33 +5,43 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicSecureTextField
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.TextFieldDecorator
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.Container
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
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.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
@ -39,6 +49,7 @@ import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.google.protobuf.value
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -191,6 +202,159 @@ fun SearchEditTextBox(
)
}
/**
* A modified [BasicTextField] that looks & fits better with TV material controls
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditTextBox(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
leadingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
readOnly: Boolean = false,
height: Dp = 40.dp,
isInputValid: (String) -> Boolean = { true },
supportingText: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val colors =
TextFieldDefaults.colors(
unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f),
errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.75f),
errorTextColor = MaterialTheme.colorScheme.onErrorContainer,
)
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
value = value,
modifier =
modifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = height,
).height(height),
onValueChange = onValueChange,
enabled = enabled,
readOnly = readOnly,
textStyle = MaterialTheme.typography.bodyLarge.merge(MaterialTheme.colorScheme.onPrimaryContainer),
cursorBrush = SolidColor(colors.cursorColor),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = true,
maxLines = 1,
minLines = 1,
visualTransformation =
if (keyboardOptions.keyboardType == KeyboardType.Password ||
keyboardOptions.keyboardType == KeyboardType.NumberPassword
) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
decorationBox =
@Composable { innerTextField ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox(
value = value,
visualTransformation =
if (keyboardOptions.keyboardType == KeyboardType.Password ||
keyboardOptions.keyboardType == KeyboardType.NumberPassword
) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
innerTextField = innerTextField,
placeholder = placeholder,
label = null,
leadingIcon = leadingIcon,
trailingIcon = null,
prefix = null,
suffix = null,
supportingText = supportingText,
shape = CircleShape,
singleLine = true,
enabled = enabled,
isError = false,
interactionSource = interactionSource,
colors = colors,
contentPadding =
PaddingValues(
horizontal = 8.dp,
vertical = 10.dp,
),
container = {
Container(
enabled = enabled,
isError = !isInputValid.invoke(value),
interactionSource = interactionSource,
modifier = Modifier,
colors = colors,
shape = CircleShape,
focusedIndicatorLineThickness = 4.dp,
unfocusedIndicatorLineThickness = 0.dp,
)
},
)
},
)
}
}
/**
* And [EditTextBox] styles for searches
*/
@Composable
fun SearchEditTextBox(
value: String,
onValueChange: (String) -> Unit,
onSearchClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
height: Dp = 40.dp,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
EditTextBox(
value,
onValueChange,
modifier,
keyboardOptions =
KeyboardOptions(
autoCorrectEnabled = false,
imeAction = ImeAction.Search,
),
keyboardActions =
KeyboardActions(
onSearch = {
onSearchClick.invoke()
this.defaultKeyboardAction(ImeAction.Search)
},
),
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
},
enabled = enabled,
readOnly = readOnly,
height = height,
interactionSource = interactionSource,
)
}
@PreviewTvSpec
@Composable
private fun EditTextBoxPreview() {
@ -199,6 +363,10 @@ private fun EditTextBoxPreview() {
EditTextBox(
state = rememberTextFieldState("string"),
)
EditTextBox(
value = "string",
onValueChange = {},
)
SearchEditTextBox(
state = rememberTextFieldState("search query"),
onSearchClick = { },

View file

@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.CircularProgressIndicator
@ -20,6 +20,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -133,7 +134,7 @@ fun PlaylistList(
properties = DialogProperties(usePlatformDefaultWidth = false),
elevation = 10.dp,
) {
val playlistName = rememberTextFieldState()
var playlistName by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
@ -148,7 +149,8 @@ fun PlaylistList(
text = stringResource(R.string.name),
)
EditTextBox(
state = playlistName,
value = playlistName,
onValueChange = { playlistName = it },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
@ -156,10 +158,17 @@ fun PlaylistList(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
onKeyboardAction = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName.text.toString())
},
keyboardActions =
KeyboardActions(
onDone = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName)
},
),
// onKeyboardAction = {
// showCreateDialog = false
// onCreatePlaylist.invoke(playlistName.text.toString())
// },
modifier =
Modifier
.focusRequester(focusRequester)
@ -168,9 +177,9 @@ fun PlaylistList(
Button(
onClick = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName.text.toString())
onCreatePlaylist.invoke(playlistName)
},
enabled = playlistName.text.isNotNullOrBlank(),
enabled = playlistName.isNotNullOrBlank(),
modifier = Modifier,
) {
Text(text = stringResource(R.string.submit))

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -172,7 +171,8 @@ fun SearchPage(
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
val query = rememberTextFieldState()
// val query = rememberTextFieldState()
var query by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
var position by rememberPosition()
@ -180,7 +180,7 @@ fun SearchPage(
LaunchedEffect(query) {
delay(750L)
viewModel.search(query.text.toString())
viewModel.search(query)
}
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
@ -214,9 +214,10 @@ fun SearchPage(
focusManager.moveFocus(FocusDirection.Next)
}
SearchEditTextBox(
state = query,
value = query,
onValueChange = { query = it },
onSearchClick = {
viewModel.search(query.text.toString())
viewModel.search(query)
searchClicked = true
},
modifier =

View file

@ -11,14 +11,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -102,7 +106,8 @@ fun DownloadSubtitlesContent(
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
val lang = rememberTextFieldState(language)
// val lang = rememberTextFieldState(language)
var lang by rememberSaveable { mutableStateOf("") }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -113,10 +118,17 @@ fun DownloadSubtitlesContent(
color = MaterialTheme.colorScheme.onSurface,
)
EditTextBox(
state = lang,
onKeyboardAction = {
onSearch(lang.text.toString())
},
value = lang,
onValueChange = { lang = it },
keyboardActions =
KeyboardActions(
onSearch = {
onSearch(lang)
},
),
// onKeyboardAction = {
// onSearch(lang.text.toString())
// },
keyboardOptions =
KeyboardOptions(
imeAction = ImeAction.Search,

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -38,9 +39,14 @@ fun StringInputDialog(
elevation: Dp = 3.dp,
) {
val mutableValue = rememberTextFieldState(input.value ?: "")
var mutableText by remember { mutableStateOf(input.value ?: "") }
// var mutableValue by remember { mutableStateOf(input.value ?: "") }
val onDone = {
onSave.invoke(mutableValue.text.toString())
if (input.maxLines > 1) {
onSave.invoke(mutableValue.text.toString())
} else {
onSave.invoke(mutableText)
}
}
var showConfirm by remember { mutableStateOf(false) }
Dialog(
@ -79,17 +85,34 @@ fun StringInputDialog(
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier,
)
EditTextBox(
state = mutableValue,
keyboardOptions = input.keyboardOptions,
onKeyboardAction = {
onDone.invoke()
},
leadingIcon = null,
isInputValid = { true },
maxLines = input.maxLines,
modifier = Modifier.fillMaxWidth(),
)
if (input.maxLines > 1) {
EditTextBox(
state = mutableValue,
keyboardOptions = input.keyboardOptions,
onKeyboardAction = {
onDone.invoke()
},
leadingIcon = null,
isInputValid = { true },
maxLines = input.maxLines,
modifier = Modifier.fillMaxWidth(),
)
} else {
EditTextBox(
value = mutableText,
onValueChange = { mutableText = it },
keyboardOptions = input.keyboardOptions,
keyboardActions =
KeyboardActions(
onDone = {
onDone.invoke()
},
),
leadingIcon = null,
isInputValid = { true },
modifier = Modifier.fillMaxWidth(),
)
}
Row(
horizontalArrangement = Arrangement.SpaceAround,

View file

@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -172,9 +172,11 @@ fun SwitchServerContent(
viewModel.clearAddServerState()
}
val state by viewModel.addServerState.observeAsState(LoadingState.Pending)
val url = rememberTextFieldState()
// val url = rememberTextFieldState()
var url by remember { mutableStateOf("") }
val submit = {
viewModel.addServer(url.text.toString())
// viewModel.addServer(url.text.toString())
viewModel.addServer(url)
}
BasicDialog(
onDismissRequest = {
@ -198,7 +200,8 @@ fun SwitchServerContent(
text = stringResource(R.string.enter_server_url),
)
EditTextBox(
state = url,
value = url,
onValueChange = { url = it },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@ -206,7 +209,11 @@ fun SwitchServerContent(
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go,
),
onKeyboardAction = { submit.invoke() },
keyboardActions =
KeyboardActions(
onGo = { submit.invoke() },
),
// onKeyboardAction = { submit.invoke() },
modifier =
Modifier
.focusRequester(focusRequester)
@ -226,7 +233,7 @@ fun SwitchServerContent(
}
TextButton(
onClick = { submit.invoke() },
enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending,
enabled = url.isNotNullOrBlank() && state == LoadingState.Pending,
modifier = Modifier,
) {
if (state == LoadingState.Loading) {

View file

@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -215,13 +215,15 @@ fun SwitchUserContent(
modifier = Modifier.align(Alignment.CenterHorizontally),
)
} else {
val username = rememberTextFieldState()
val password = rememberTextFieldState()
// val username = rememberTextFieldState()
// val password = rememberTextFieldState()
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
val onSubmit = {
viewModel.login(
server,
username.text.toString(),
password.text.toString(),
username,
password,
)
}
val focusRequester = remember { FocusRequester() }
@ -242,7 +244,8 @@ fun SwitchUserContent(
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(
state = username,
value = username,
onValueChange = { username = it },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@ -250,9 +253,15 @@ fun SwitchUserContent(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next,
),
onKeyboardAction = {
passwordFocusRequester.tryRequestFocus()
},
keyboardActions =
KeyboardActions(
onNext = {
passwordFocusRequester.tryRequestFocus()
},
),
// onKeyboardAction = {
// passwordFocusRequester.tryRequestFocus()
// },
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier.focusRequester(focusRequester),
)
@ -265,12 +274,12 @@ fun SwitchUserContent(
text = "Password",
modifier = Modifier.padding(end = 8.dp),
)
LaunchedEffect(password.text) {
LaunchedEffect(password) {
viewModel.clearSwitchUserState()
}
EditTextBox(
isPassword = true,
state = password,
value = password,
onValueChange = { password = it },
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@ -278,7 +287,10 @@ fun SwitchUserContent(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Go,
),
onKeyboardAction = { onSubmit.invoke() },
keyboardActions =
KeyboardActions(
onGo = { onSubmit.invoke() },
),
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier.focusRequester(passwordFocusRequester),
)
@ -286,7 +298,7 @@ fun SwitchUserContent(
TextButton(
stringRes = R.string.login,
onClick = { onSubmit.invoke() },
enabled = username.text.isNotNullOrBlank(),
enabled = username.isNotNullOrBlank(),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
}