diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt index 67faa648..a85d9fc9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt @@ -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 = { }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt index 65b3e43f..f94f7381 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt @@ -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)) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index 1a39c51d..19b04d0b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -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 = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt index efb0c1fe..e145d380 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt index 4229f78d..0e40ad24 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt index c1bc542e..6fafeecc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index 2134e481..c734aed2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -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), ) }