From abcef9a86efea49ef0be7a1220c6be59b4afaa6f Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:40:09 +0800 Subject: [PATCH] Update Compose snapshot and migrate to BTF2 (#1474) * Update Compose snapshot and remove workaround * Sync SearchBarInputField with upstream * Migrate to BTF2 text fields * Fix search page range error text container color * Update snapshot to fix a SieveCache crash https://r.android.com/3199791 --- app/build.gradle.kts | 3 +- .../androidx/compose/material3/SearchBar.kt | 104 ---------- .../compose/material3/SearchBarInputField.kt | 191 ++++++++++++++++++ .../hippo/ehviewer/ui/login/SignInScreen.kt | 38 ++-- .../hippo/ehviewer/ui/main/SearchFilter.kt | 2 + .../ehviewer/ui/screen/SearchBarScreen.kt | 2 +- .../hippo/ehviewer/ui/settings/EhScreen.kt | 8 +- .../ehviewer/ui/settings/FilterScreen.kt | 23 ++- .../com/hippo/ehviewer/ui/tools/Dialog.kt | 51 ++--- .../tachiyomi/ui/reader/ReaderPageSheet.kt | 6 +- settings.gradle.kts | 2 +- 11 files changed, 260 insertions(+), 170 deletions(-) delete mode 100644 app/src/main/kotlin/androidx/compose/material3/SearchBar.kt create mode 100644 app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c10751d00..6c1da8c91 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ plugins { val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a") android { - compileSdk = if (isRelease) 35 else 34 + compileSdk = 35 buildToolsVersion = "35.0.0" ndkVersion = "27.0.11902837-rc1" androidResources.generateLocaleConfig = true @@ -284,6 +284,7 @@ kotlin { "-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi", "-opt-in=androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi", "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", diff --git a/app/src/main/kotlin/androidx/compose/material3/SearchBar.kt b/app/src/main/kotlin/androidx/compose/material3/SearchBar.kt deleted file mode 100644 index 4103f44df..000000000 --- a/app/src/main/kotlin/androidx/compose/material3/SearchBar.kt +++ /dev/null @@ -1,104 +0,0 @@ -@file:Suppress("ktlint:standard:property-naming") - -package androidx.compose.material3 - -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.SearchBarDefaults.InputFieldHeight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay - -@Composable -fun SearchBarInputField( - state: TextFieldState, - onSearch: () -> Unit, - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - label: @Composable (() -> Unit)? = null, - placeholder: @Composable (() -> Unit)? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - colors: TextFieldColors = SearchBarDefaults.inputFieldColors(), - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, -) { - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - val focused by interactionSource.collectIsFocusedAsState() - val textColor = LocalTextStyle.current.color.takeOrElse { - if (focused) colors.focusedTextColor else colors.unfocusedTextColor - } - - BasicTextField( - state = state, - modifier = modifier.sizeIn( - minWidth = SearchBarMinWidth, - maxWidth = SearchBarMaxWidth, - minHeight = InputFieldHeight, - ).focusRequester(focusRequester).onFocusChanged { - if (it.isFocused) onExpandedChange(true) - }.fillMaxWidth(), - enabled = enabled, - lineLimits = TextFieldLineLimits.SingleLine, - textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)), - cursorBrush = SolidColor(colors.cursorColor), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - onKeyboardAction = { onSearch() }, - interactionSource = interactionSource, - decorator = @Composable { innerTextField -> - TextFieldDefaults.DecorationBox( - value = state.text.toString(), - innerTextField = innerTextField, - enabled = enabled, - singleLine = true, - visualTransformation = VisualTransformation.None, - interactionSource = interactionSource, - placeholder = placeholder, - leadingIcon = leadingIcon?.let { leading -> { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } }, - trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } }, - shape = SearchBarDefaults.inputFieldShape, - colors = colors, - container = {}, - ) - }, - ) - - val shouldClearFocus = !expanded && focused - LaunchedEffect(expanded) { - if (shouldClearFocus) { - // Not strictly needed according to the motion spec, but since the animation - // already has a delay, this works around b/261632544. - delay(AnimationDelayMillis.toLong()) - focusManager.clearFocus() - } - } -} - -val SearchBarIconOffsetX = 4.dp -val SearchBarMinWidth = 360.dp -val SearchBarMaxWidth = 720.dp -val SearchBarHorizontalPadding = 16.dp -const val AnimationDelayMillis = 100 diff --git a/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt b/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt new file mode 100644 index 000000000..1b9c5dd64 --- /dev/null +++ b/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt @@ -0,0 +1,191 @@ +package androidx.compose.material3 + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.SearchBarDefaults.InputFieldHeight +import androidx.compose.material3.SearchBarDefaults.inputFieldColors +import androidx.compose.material3.SearchBarDefaults.inputFieldShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +/** + * A text field to input a query in a search bar. + * + * @param state [TextFieldState] that holds the internal editing state of the input field. + * @param onSearch the callback to be invoked when the input service triggers the + * [ImeAction.Search] action. The current query in the [state] comes as a parameter of the + * callback. + * @param expanded whether the search bar is expanded and showing search results. + * @param onExpandedChange the callback to be invoked when the search bar's expanded state is + * changed. + * @param modifier the [Modifier] to be applied to this input field. + * @param enabled the enabled state of this input field. When `false`, this component will not + * respond to user input, and it will appear visually disabled and disabled to accessibility + * services. + * @param readOnly controls the editable state of the input field. When `true`, the field cannot + * be modified. However, a user can focus it and copy text from it. + * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. + * @param placeholder the placeholder to be displayed when the input text is empty. + * @param leadingIcon the leading icon to be displayed at the start of the input field. + * @param trailingIcon the trailing icon to be displayed at the end of the input field. + * @param prefix the optional prefix to be displayed before the input text. + * @param suffix the optional suffix to be displayed after the input text. + * @param inputTransformation optional [InputTransformation] that will be used to transform + * changes to the [TextFieldState] made by the user. The transformation will be applied to + * changes made by hardware and software keyboard events, pasting or dropping text, + * accessibility services, and tests. The transformation will _not_ be applied when changing + * the [state] programmatically, or when the transformation is changed. If the transformation + * is changed on an existing text field, it will be applied to the next user edit. The + * transformation will not immediately affect the current [state]. + * @param outputTransformation optional [OutputTransformation] that transforms how the contents + * of the text field are presented. + * @param scrollState scroll state that manages the horizontal scroll of the input field. + * @param shape the shape of the input field. + * @param colors [TextFieldColors] that will be used to resolve the colors used for this input + * field in different states. See [SearchBarDefaults.inputFieldColors]. + * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and + * emitting [Interaction]s for this input field. You can use this to change the search bar's + * appearance or preview the search bar in different states. Note that if `null` is provided, + * interactions will still happen internally. + */ +@ExperimentalMaterial3Api +@Composable +fun SearchBarInputField( + state: TextFieldState, + onSearch: (String) -> Unit, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + inputTransformation: InputTransformation? = null, + outputTransformation: OutputTransformation? = null, + scrollState: ScrollState = rememberScrollState(), + shape: Shape = inputFieldShape, + colors: TextFieldColors = inputFieldColors(), + interactionSource: MutableInteractionSource? = null, +) { + @Suppress("NAME_SHADOWING") + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + + val focused = interactionSource.collectIsFocusedAsState().value + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + val textColor = textStyle.color.takeOrElse { + if (focused) colors.focusedTextColor else colors.unfocusedTextColor + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + + BasicTextField( + state = state, + modifier = modifier + .sizeIn( + minWidth = SearchBarMinWidth, + maxWidth = SearchBarMaxWidth, + minHeight = InputFieldHeight, + ) + .focusRequester(focusRequester) + .onFocusChanged { if (it.isFocused) onExpandedChange(true) } + .fillMaxWidth(), + enabled = enabled, + readOnly = readOnly, + lineLimits = TextFieldLineLimits.SingleLine, + textStyle = mergedTextStyle, + cursorBrush = SolidColor(colors.cursorColor), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + onKeyboardAction = { onSearch(state.text.toString()) }, + interactionSource = interactionSource, + inputTransformation = inputTransformation, + outputTransformation = outputTransformation, + scrollState = scrollState, + decorator = TextFieldDefaults.decorator( + state = state, + enabled = enabled, + lineLimits = TextFieldLineLimits.SingleLine, + outputTransformation = outputTransformation, + interactionSource = interactionSource, + placeholder = placeholder, + leadingIcon = leadingIcon?.let { leading -> + { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } + }, + trailingIcon = trailingIcon?.let { trailing -> + { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } + }, + prefix = prefix, + suffix = suffix, + colors = colors, + contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(), + container = { + val containerColor = + animateColorAsState( + targetValue = if (focused) colors.focusedContainerColor else colors.unfocusedContainerColor, + animationSpec = MaterialTheme.motionScheme.rememberFastEffectsSpec(), + label = "ContainerColor", + ) + Box(Modifier.textFieldBackground(containerColor::value, shape)) + }, + ), + ) + + val shouldClearFocus = !expanded && focused + LaunchedEffect(expanded) { + if (shouldClearFocus) { + // Not strictly needed according to the motion spec, but since the animation + // already has a delay, this works around b/261632544. + delay(AnimationDelayMillis.toLong()) + focusManager.clearFocus() + } + } +} + +private fun Modifier.textFieldBackground( + color: ColorProducer, + shape: Shape, +) = drawWithCache { + val outline = shape.createOutline(size, layoutDirection, this) + onDrawBehind { drawOutline(outline, color = color()) } +} + +private val SearchBarMinWidth = 360.dp +private val SearchBarMaxWidth = 720.dp +private val SearchBarIconOffsetX = 4.dp + +@Suppress("ktlint:standard:property-naming") +private const val AnimationDelayMillis = 100 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt index d4eed7f0d..02bd779fd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/login/SignInScreen.kt @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info @@ -49,8 +51,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString 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.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp @@ -85,8 +85,8 @@ fun SignInScreen(navigator: DestinationsNavigator) { var isProgressIndicatorVisible by rememberSaveable { mutableStateOf(false) } var showUsernameError by rememberSaveable { mutableStateOf(false) } var showPasswordError by rememberSaveable { mutableStateOf(false) } - var username by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } + val username = rememberTextFieldState() + val password = rememberTextFieldState() var passwordHidden by rememberSaveable { mutableStateOf(true) } val context = LocalContext.current var signInJob by remember { mutableStateOf(null) } @@ -94,13 +94,13 @@ fun SignInScreen(navigator: DestinationsNavigator) { fun signIn() { if (signInJob?.isActive == true) return - if (username.isEmpty()) { + if (username.text.isEmpty()) { showUsernameError = true return } else { showUsernameError = false } - if (password.isEmpty()) { + if (password.text.isEmpty()) { showPasswordError = true return } else { @@ -112,7 +112,7 @@ fun SignInScreen(navigator: DestinationsNavigator) { EhUtils.signOut() signInJob = coroutineScope.launchIO { runCatching { - EhEngine.signIn(username, password) + EhEngine.signIn(username.text.toString(), password.text.toString()) }.onFailure { withUIContext { focusManager.clearFocus() @@ -141,31 +141,33 @@ fun SignInScreen(navigator: DestinationsNavigator) { @Composable fun UsernameAndPasswordTextField() { OutlinedTextField( - value = username, - onValueChange = { username = it }, + state = username, modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width)).autofill( autofillTypes = listOf(AutofillType.Username), - onFill = { username = it }, + onFill = { username.setTextAndPlaceCursorAtEnd(it) }, ), label = { Text(stringResource(R.string.username)) }, supportingText = { if (showUsernameError) Text(stringResource(R.string.error_username_cannot_empty)) }, trailingIcon = { if (showUsernameError) Icon(imageVector = Icons.Filled.Info, contentDescription = null) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, isError = showUsernameError, ) OutlinedTextField( - value = password, - onValueChange = { password = it }, + state = password, modifier = Modifier.width(dimensionResource(id = R.dimen.single_max_width)).autofill( autofillTypes = listOf(AutofillType.Password), - onFill = { password = it }, + onFill = { password.setTextAndPlaceCursorAtEnd(it) }, ), label = { Text(stringResource(R.string.password)) }, - visualTransformation = if (passwordHidden) PasswordVisualTransformation() else VisualTransformation.None, + outputTransformation = { + if (passwordHidden) { + replace(0, length, "\u2022".repeat(length)) + } + }, supportingText = { if (showPasswordError) Text(stringResource(R.string.error_password_cannot_empty)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - keyboardActions = KeyboardActions(onDone = { signIn() }), + onKeyboardAction = { signIn() }, trailingIcon = { if (showPasswordError) { Icon(imageVector = Icons.Filled.Info, contentDescription = null) @@ -176,7 +178,7 @@ fun SignInScreen(navigator: DestinationsNavigator) { } } }, - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, isError = showPasswordError, ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt index 703e68a9a..27014fbff 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/SearchFilter.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource @@ -193,6 +194,7 @@ fun SearchFilter( Icon(imageVector = Icons.Default.Info, contentDescription = null) }, colors = ListItemDefaults.colors( + containerColor = Color.Transparent, headlineColor = MaterialTheme.colorScheme.error, leadingIconColor = MaterialTheme.colorScheme.error, ), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt index 154366067..60acd57c8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.SearchBarHorizontalPadding import androidx.compose.material3.SearchBarInputField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -314,3 +313,4 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (ke } private val WhitespaceRegex = Regex("\\s+") +private val SearchBarHorizontalPadding = 16.dp diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt index ffd73719f..de689155b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -125,16 +126,15 @@ fun EhScreen(navigator: DestinationsNavigator) { if (hasSignedIn) { Column { val warning = stringResource(id = R.string.settings_eh_identity_cookies_signed) - val str = cookies.joinToString("\n") { (k, v) -> "$k: $v" } + val state = rememberTextFieldState(cookies.joinToString("\n") { (k, v) -> "$k: $v" }) Text(text = AnnotatedString.fromHtml(warning)) Spacer(modifier = Modifier.size(dimensionResource(id = R.dimen.keyline_margin))) OutlinedTextField( - value = str, - onValueChange = {}, + state = state, readOnly = true, trailingIcon = { IconButton(onClick = { - copyTextToClipboard(str, true) + copyTextToClipboard(state.text, true) // Avoid double notify user since system have done that on Tiramisu above if (!isAtLeastT) launchSnackBar(copiedToClipboard) }) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt index e42252bdb..d48d83736 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/FilterScreen.kt @@ -12,6 +12,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Help @@ -84,17 +87,17 @@ fun FilterScreen(navigator: DestinationsNavigator) { scope.launch { dialogState.dialog { cont -> val types = stringArrayResource(id = R.array.filter_entries) - var type by remember { mutableStateOf(types[0]) } - var value by remember { mutableStateOf("") } + val type = rememberTextFieldState(types[0]) + val state = rememberTextFieldState() var error by remember { mutableStateOf(null) } fun invalidateAndSave() { - if (value.isBlank()) { + if (state.text.isBlank()) { error = textIsEmpty return } error = null - val mode = FilterMode.entries[types.indexOf(type)] - val filter = Filter(mode, value) + val mode = FilterMode.entries[types.indexOf(type.text)] + val filter = Filter(mode, state.text.toString()) filter.remember { if (it) { cont.resume(Unit) @@ -129,8 +132,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { OutlinedTextField( modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable), readOnly = true, - value = type, - onValueChange = {}, + state = type, label = { Text(text = stringResource(id = R.string.filter_label)) }, @@ -146,7 +148,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { text = { Text(text = it) }, onClick = { expanded = false - type = it + type.setTextAndPlaceCursorAtEnd(it) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) @@ -156,8 +158,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { Spacer(modifier = Modifier.size(16.dp)) val isError = error != null OutlinedTextField( - value = value, - onValueChange = { value = it }, + state = state, label = { Text(text = stringResource(id = R.string.filter_text)) }, supportingText = { error?.let { Text(text = it) } }, trailingIcon = { @@ -169,7 +170,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { } }, isError = isError, - maxLines = 1, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, ), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt index da33d7444..df915ec6e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -20,6 +20,8 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -160,7 +162,7 @@ class DialogState { invalidator: (suspend Raise.(String) -> Unit)? = null, ) = dialog { cont -> val coroutineScope = rememberCoroutineScope() - var state by remember(cont) { mutableStateOf(initial) } + val state = rememberTextFieldState(initial) var error by remember(cont) { mutableStateOf(null) } AlertDialog( onDismissRequest = { @@ -169,12 +171,13 @@ class DialogState { }, confirmButton = { TextButton(onClick = { + val text = state.text.toString() if (invalidator == null) { - cont.resume(state) + cont.resume(text) } else { coroutineScope.launch { - error = either { invalidator(state) }.leftOrNull() - error ?: cont.resume(state) + error = either { invalidator(text) }.leftOrNull() + error ?: cont.resume(text) } } }) { @@ -184,10 +187,9 @@ class DialogState { title = title.ifNotNullThen { Text(text = title!!) }, text = { OutlinedTextField( - value = state, - onValueChange = { state = it }, - label = hint.ifNotNullThen { - Text(text = hint!!) + state = state, + label = hint?.let { + @Composable { Text(text = it) } }, trailingIcon = error.ifNotNullThen { Icon( @@ -215,19 +217,20 @@ class DialogState { invalidator: (suspend Raise.(String, Boolean) -> Unit)? = null, ): Pair = dialog { cont -> val coroutineScope = rememberCoroutineScope() - var state by remember(cont) { mutableStateOf(initial) } + val state = rememberTextFieldState(initial) var error by remember(cont) { mutableStateOf(null) } var checkedState by remember { mutableStateOf(checked) } AlertDialog( onDismissRequest = { cont.cancel() }, confirmButton = { TextButton(onClick = { + val text = state.text.toString() if (invalidator == null) { - cont.resume(state to checkedState) + cont.resume(text to checkedState) } else { coroutineScope.launch { - error = either { invalidator(state, checkedState) }.leftOrNull() - error ?: cont.resume(state to checkedState) + error = either { invalidator(text, checkedState) }.leftOrNull() + error ?: cont.resume(text to checkedState) } } }) { @@ -240,10 +243,9 @@ class DialogState { verticalArrangement = Arrangement.spacedBy(16.dp), ) { OutlinedTextField( - value = state, - onValueChange = { state = it }, - label = hint.ifNotNullThen { - Text(text = stringResource(id = hint!!)) + state = state, + label = hint?.let { + { Text(text = stringResource(id = it)) } }, trailingIcon = error.ifNotNullThen { Icon( @@ -526,7 +528,7 @@ class DialogState { leadingContent = { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) }, - colors = ListItemDefaults.colors(containerColor = Color.Unspecified), + colors = ListItemDefaults.colors(containerColor = Color.Transparent), ) } } @@ -545,22 +547,21 @@ class DialogState { modifier = Modifier.fillMaxWidth().aspectRatio(1F), placeFirstItemInCenter = true, ) { - var note by remember { mutableStateOf(initialNote) } + val note = rememberTextFieldState(initialNote) TextField( - value = note, - onValueChange = { note = it }, + state = note, modifier = Modifier.fillMaxWidth(0.45F).aspectRatio(1F), label = { Text(text = stringResource(id = hint)) }, trailingIcon = { - if (note.isNotEmpty()) { - IconButton(onClick = { note = "" }) { + if (note.text.isNotEmpty()) { + IconButton(onClick = { note.clearText() }) { Icon(imageVector = Icons.Default.Close, contentDescription = null) } } }, supportingText = { Text( - text = "${note.toByteArray().size} / $maxChar", + text = "${note.text.toString().toByteArray().size} / $maxChar", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.End, ) @@ -573,7 +574,7 @@ class DialogState { ) items.forEachIndexed { index, (icon, text) -> Column( - modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) } + modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note.text.toString()) } .fillMaxWidth(0.2F), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -607,7 +608,7 @@ private fun CheckableItem(text: String, checked: Boolean, modifier: Modifier = M trailingContent = checked.ifTrueThen { Icon(imageVector = Icons.Default.Check, contentDescription = null, tint = checkedColor) }, - colors = ListItemDefaults.colors(containerColor = Color.Unspecified), + colors = ListItemDefaults.colors(containerColor = Color.Transparent), ) } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt index c70a8e8b3..e0eef8515 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding @@ -48,10 +47,7 @@ fun ReaderPageSheetMeta( Spacer(modifier = Modifier.size(32.dp)) Text(text = stringResource(id = text)) } - Column( - modifier = Modifier.fillMaxSize() // Workaround for https://issuetracker.google.com/341594885 - .verticalScroll(rememberScrollState()).navigationBarsPadding(), - ) { + Column(modifier = Modifier.verticalScroll(rememberScrollState()).navigationBarsPadding()) { Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry) Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin) Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7974338f0..fccf58178 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,7 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven("https://androidx.dev/snapshots/builds/12043948/artifacts/repository") + maven("https://androidx.dev/snapshots/builds/12160189/artifacts/repository") } }