diff --git a/app/src/main/java/com/example/research/ui/settings/components/DictionaryUrlInputContent.kt b/app/src/main/java/com/example/research/ui/settings/components/DictionaryUrlInputContent.kt index 1abfb41..44774c9 100644 --- a/app/src/main/java/com/example/research/ui/settings/components/DictionaryUrlInputContent.kt +++ b/app/src/main/java/com/example/research/ui/settings/components/DictionaryUrlInputContent.kt @@ -3,6 +3,9 @@ package com.example.research.ui.settings.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.verticalScroll import androidx.compose.ui.res.painterResource import androidx.compose.material3.* @@ -16,7 +19,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.example.research.R import com.example.research.core.domain.model.DictionarySource @@ -64,6 +66,20 @@ private fun validateDictionaryUrl(context: UrlValidationContext): String? { } } +private val urlInputTransformation = InputTransformation { + val text = toString() + val filtered = text + .take(2048) + .filter { char -> + !char.isISOControl() && + char != '\u0000' && + char.code !in 128..<160 + } + if (filtered != text) { + replace(0, length, filtered) + } +} + @Composable fun DictionaryUrlInputContent( existingSources: List, @@ -71,7 +87,7 @@ fun DictionaryUrlInputContent( onCancel: () -> Unit, modifier: Modifier = Modifier ) { - val urls = remember { mutableStateListOf(TextFieldValue("")) } + val urls = remember { mutableStateListOf(TextFieldState("")) } val validationErrors = remember { mutableStateMapOf() } val scrollState = rememberScrollState() val focusManager = LocalFocusManager.current @@ -96,8 +112,8 @@ fun DictionaryUrlInputContent( // Cache prefixes for all input URLs to avoid repeated extraction during recomposition val inputUrlPrefixes = remember { derivedStateOf { - urls.associate { urlValue -> - val urlString = urlValue.text.trim() + urls.associate { urlState -> + val urlString = urlState.trimmedText() urlString to if (urlString.isNotEmpty()) { DictionarySource.extractPrefix(urlString) } else null @@ -119,10 +135,10 @@ fun DictionaryUrlInputContent( style = MaterialTheme.typography.headlineSmall ) - urls.forEachIndexed { index, urlValue -> + urls.forEachIndexed { index, urlState -> val isLastItem = index == urls.lastIndex - - val urlString = urlValue.text.trim() + + val urlString = urlState.trimmedText() val cachedPrefix = inputUrlPrefixes.value[urlString] val realtimeError = remember(urlString, existingPrefixes, urls.size, cachedPrefix) { validateDictionaryUrl( @@ -130,7 +146,7 @@ fun DictionaryUrlInputContent( url = urlString, existingSources = existingSources, existingPrefixes = existingPrefixes, - urlsInForm = urls.map { it.text.trim() }, + urlsInForm = urls.map { it.trimmedText() }, urlPrefix = cachedPrefix, errorMessages = errorMessages ) @@ -140,20 +156,8 @@ fun DictionaryUrlInputContent( val displayError = validationErrors[index] ?: realtimeError OutlinedTextField( - value = urlValue, - onValueChange = { newValue -> - val filtered = newValue.copy( - text = newValue.text - .take(2048) - .filter { char -> - !char.isISOControl() && - char != '\u0000' && - char.code !in 128..<160 - } - ) - urls[index] = filtered - validationErrors.remove(index) - }, + state = urlState, + inputTransformation = urlInputTransformation, label = { Text(if (index == 0) stringResource(R.string.dictionary_source_url_hint) else "URL ${index + 1}") }, isError = displayError != null, supportingText = displayError?.let { { Text(it) } }, @@ -168,7 +172,7 @@ fun DictionaryUrlInputContent( Modifier } ), - singleLine = true, + lineLimits = TextFieldLineLimits.SingleLine, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, imeAction = if (isLastItem) ImeAction.Done else ImeAction.Next @@ -188,7 +192,7 @@ fun DictionaryUrlInputContent( TextButton( onClick = { - urls.add(TextFieldValue("")) + urls.add(TextFieldState("")) }, modifier = Modifier .align(Alignment.Start) @@ -224,8 +228,8 @@ fun DictionaryUrlInputContent( val validUrls = mutableListOf() val seenUrls = mutableSetOf() - urls.forEachIndexed { index, urlValue -> - val urlString = urlValue.text.trim() + urls.forEachIndexed { index, urlState -> + val urlString = urlState.trimmedText() if (urlString.isBlank()) { if (urls.size == 1) { validationErrors[index] = errorMessages.empty @@ -263,9 +267,9 @@ fun DictionaryUrlInputContent( .weight(1f) .testTag("add_confirm_button") .semantics { testTagsAsResourceId = true }, - enabled = urls.any { it.text.trim().isNotBlank() } && - urls.none { urlValue -> - val urlString = urlValue.text.trim() + enabled = urls.any { it.trimmedText().isNotBlank() } && + urls.none { urlState -> + val urlString = urlState.trimmedText() if (urlString.isEmpty()) { false } else { @@ -275,7 +279,7 @@ fun DictionaryUrlInputContent( url = urlString, existingSources = existingSources, existingPrefixes = existingPrefixes, - urlsInForm = urls.map { it.text.trim() }, + urlsInForm = urls.map { it.trimmedText() }, urlPrefix = urlPrefix, errorMessages = errorMessages ) @@ -289,3 +293,5 @@ fun DictionaryUrlInputContent( Spacer(modifier = Modifier.height(8.dp)) } } + +private fun TextFieldState.trimmedText(): String = text.trim().toString()