diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ManagerViewModel.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ManagerViewModel.kt index 09991118f..b61b0cf91 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ManagerViewModel.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ManagerViewModel.kt @@ -1,5 +1,8 @@ package com.github.jing332.tts_server_android.compose.systts.replace +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.jing332.tts_server_android.data.appDb @@ -12,27 +15,37 @@ import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.launch internal class ManagerViewModel : ViewModel() { + private var allList = listOf() + private val _list = MutableStateFlow>(emptyList()) val list: MutableStateFlow> get() = _list + var searchType by mutableStateOf(SearchType.GROUP_NAME) + var searchText by mutableStateOf("") + init { viewModelScope.launch(Dispatchers.IO) { appDb.replaceRuleDao.updateAllOrder() appDb.replaceRuleDao.flowAllGroupWithReplaceRules().conflate().collectLatest { - _list.value = it + allList = it + updateSearchResult() } } } - fun updateSearchResult(text: String, type: SearchType) { - if (list.value.isEmpty() || text.isBlank()) { - _list.value = appDb.replaceRuleDao.allGroupWithReplaceRules() + fun updateSearchResult( + text: String = searchText, + type: SearchType = searchType, + src: List = allList + ) { + if (src.isEmpty() || text.isBlank()) { + _list.value = src return } val resultList = mutableListOf() - list.value.forEach { + src.forEach { val subList = mutableListOf() val groupWithRules = GroupWithReplaceRule(it.group, subList) resultList.add(groupWithRules) diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ReplaceRuleManagerScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ReplaceRuleManagerScreen.kt index 85765b5e9..b298b8f5c 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ReplaceRuleManagerScreen.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/ReplaceRuleManagerScreen.kt @@ -2,12 +2,17 @@ package com.github.jing332.tts_server_android.compose.systts.replace import android.os.Bundle import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AddCard @@ -21,6 +26,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -29,9 +35,9 @@ 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.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -107,24 +113,56 @@ internal fun ManagerScreen(vm: ManagerViewModel = viewModel(), finish: () -> Uni } - val models by vm.list.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { - var type by rememberSaveable { mutableStateOf(SearchType.NAME) } - var text by rememberSaveable { mutableStateOf("") } - LaunchedEffect(text, type) { - vm.updateSearchResult(text, type) + LaunchedEffect(vm.searchText, vm.searchType) { + vm.updateSearchResult() + } + Row( + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainer) + ) { + SearchTextField( + modifier = Modifier.weight(1f), + value = vm.searchText, + onValueChange = { vm.searchText = it }, + searchType = vm.searchType, + onSearchTypeChange = { vm.searchType = it } + ) + var showAddOptions by remember { mutableStateOf(false) } + IconButton(onClick = { showAddOptions = true }) { + Icon(Icons.Default.Add, stringResource(id = R.string.add_config)) + DropdownMenu( + expanded = showAddOptions, + onDismissRequest = { showAddOptions = false }) { + DropdownMenuItem( + text = { Text(stringResource(id = R.string.add_config)) }, + onClick = { + showAddOptions = false + navigateToEdit() + }, + leadingIcon = { + Icon(Icons.Default.PlaylistAdd, null) + } + ) + DropdownMenuItem( + text = { Text(stringResource(id = R.string.add_group)) }, + onClick = { + showAddOptions = false + showAddGroupDialog = true + }, + leadingIcon = { + Icon(Icons.Default.AddCard, null) + } + ) + } + } } - SearchTextField( - value = text, - onValueChange = { text = it }, - searchType = type, - onSearchTypeChange = { type = it } - ) }, navigationIcon = { IconButton(onClick = finish) { @@ -132,34 +170,7 @@ internal fun ManagerScreen(vm: ManagerViewModel = viewModel(), finish: () -> Uni } }, actions = { - var showAddOptions by remember { mutableStateOf(false) } - IconButton(onClick = { showAddOptions = true }) { - Icon(Icons.Default.Add, stringResource(id = R.string.add_config)) - DropdownMenu( - expanded = showAddOptions, - onDismissRequest = { showAddOptions = false }) { - DropdownMenuItem( - text = { Text(stringResource(id = R.string.add_config)) }, - onClick = { - showAddOptions = false - navigateToEdit() - }, - leadingIcon = { - Icon(Icons.Default.PlaylistAdd, null) - } - ) - DropdownMenuItem( - text = { Text(stringResource(id = R.string.add_group)) }, - onClick = { - showAddOptions = false - showAddGroupDialog = true - }, - leadingIcon = { - Icon(Icons.Default.AddCard, null) - } - ) - } - } + var showOptions by remember { mutableStateOf(false) } IconButton(onClick = { showOptions = true }) { @@ -256,7 +267,7 @@ internal fun ManagerScreen(vm: ManagerViewModel = viewModel(), finish: () -> Uni onEdit = { showGroupEditDialog = g }, onDelete = { appDb.replaceRuleDao.delete(*groupWithRules.list.toTypedArray()) }, onExport = { showExportSheet = listOf(groupWithRules) }, - onSort = { showSortDialog = groupWithRules.list} + onSort = { showSortDialog = groupWithRules.list } ) } } diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SearchTextField.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SearchTextField.kt index 960b5a27c..e3325e9b0 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SearchTextField.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SearchTextField.kt @@ -2,26 +2,33 @@ package com.github.jing332.tts_server_android.compose.systts.replace import android.os.Parcelable import androidx.annotation.StringRes +import androidx.compose.foundation.background import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountTree import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -31,79 +38,88 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.github.jing332.tts_server_android.R import com.github.jing332.tts_server_android.compose.theme.AppTheme +import com.github.jing332.tts_server_android.compose.widgets.DenseTextField import kotlinx.parcelize.Parcelize @Parcelize -enum class SearchType(@StringRes val strId: Int) : Parcelable { - GROUP_NAME(R.string.group_name), +internal enum class SearchType(@StringRes val strId: Int) : Parcelable { NAME(R.string.display_name), PATTERN(R.string.replace_rule), - REPLACEMENT(R.string.systts_replace_as), + REPLACEMENT(R.string.replacement), + GROUP_NAME(R.string.group_name), } @Composable -fun SearchTextField( +internal fun SearchTextField( + modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, searchType: SearchType, onSearchTypeChange: (SearchType) -> Unit ) { - TextField( - value = value, - onValueChange = onValueChange, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - ), - singleLine = true, - leadingIcon = { - var showTypeOptions by remember { mutableStateOf(false) } - IconButton(onClick = { showTypeOptions = true }) { - Icon( - Icons.Default.AccountTree, stringResource(id = R.string.type) - ) + CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) { + DenseTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + shape = MaterialTheme.shapes.extraLarge, + placeholder = { Text(stringResource(id = R.string.search_filter)) }, + singleLine = true, + leadingIcon = { + var showTypeOptions by remember { mutableStateOf(false) } + IconButton(onClick = { showTypeOptions = true }) { + Icon( + Icons.Default.AccountTree, stringResource(id = R.string.type) + ) - DropdownMenu( - expanded = showTypeOptions, - onDismissRequest = { showTypeOptions = false }) { + DropdownMenu( + expanded = showTypeOptions, + onDismissRequest = { showTypeOptions = false }) { - @Composable - fun RadioMenuItem( - isSelected: Boolean, - title: @Composable () -> Unit, - onClick: () -> Unit - ) { - DropdownMenuItem( - modifier = Modifier.semantics { - role = Role.RadioButton - }, - text = title, - onClick = { - showTypeOptions = false - onClick() - }, - leadingIcon = { - RadioButton( - modifier = Modifier.focusable(false), - selected = isSelected, - onClick = null - ) - } - ) - } + @Composable + fun RadioMenuItem( + isSelected: Boolean, + title: @Composable () -> Unit, + onClick: () -> Unit + ) { + DropdownMenuItem( + modifier = Modifier.semantics { + role = Role.RadioButton + }, + text = title, + onClick = { + showTypeOptions = false + onClick() + }, + leadingIcon = { + RadioButton( + modifier = Modifier.focusable(false), + selected = isSelected, + onClick = null + ) + } + ) + } - SearchType.values().forEach { - RadioMenuItem( - isSelected = it == searchType, - title = { Text(stringResource(id = it.strId)) }, - onClick = { onSearchTypeChange(it) } - ) - } + SearchType.values().forEach { + RadioMenuItem( + isSelected = it == searchType, + title = { Text(stringResource(id = it.strId)) }, + onClick = { onSearchTypeChange(it) } + ) + } + } } } - } - ) + ) + } } @Preview diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SortDialog.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SortDialog.kt index c032e6017..0f58e5223 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SortDialog.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/SortDialog.kt @@ -16,7 +16,7 @@ internal enum class SortType(@StringRes val strId: Int) { CREATE_TIME(R.string.created_time_id), NAME(R.string.display_name), PATTERN(R.string.replace_rule), - REPLACEMENT(R.string.systts_replace_as), + REPLACEMENT(R.string.replacement), ENABLED(R.string.enabled), USE_REGEX(R.string.systts_replace_use_regex), diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/edit/RuleEditScreen.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/edit/RuleEditScreen.kt index 8d9d840eb..66324ecfa 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/edit/RuleEditScreen.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/systts/replace/edit/RuleEditScreen.kt @@ -329,7 +329,7 @@ private fun Screen( Icon(Icons.Filled.Abc, stringResource(R.string.systts_replace_insert_pinyin)) } }) - OutlinedTextField(label = { Text(stringResource(R.string.systts_replace_as)) }, + OutlinedTextField(label = { Text(stringResource(R.string.replacement)) }, value = replacementTextFieldValue, onValueChange = { replacementTextFieldValue = it diff --git a/app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseOutlinedField.kt b/app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseTextField.kt similarity index 59% rename from app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseOutlinedField.kt rename to app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseTextField.kt index aab52a521..e4bc3f7a5 100644 --- a/app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseOutlinedField.kt +++ b/app/src/main/java/com/github/jing332/tts_server_android/compose/widgets/DenseTextField.kt @@ -3,6 +3,7 @@ package com.github.jing332.tts_server_android.compose.widgets import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -12,6 +13,7 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -139,3 +141,100 @@ fun DenseOutlinedField( ) // } } + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DenseTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = TextFieldDefaults.shape, + colors: TextFieldColors = TextFieldDefaults.colors() +) { + // If color is not provided via the text style, use content color as a default + val textColor = textStyle.color.takeOrElse { + textColor(enabled, isError, interactionSource).value + } + val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) + +// CompositionLocalProvider(LocalTextSelectionColors provides colors.selectionColors) { + BasicTextField( + value = value, + modifier = if (label != null) { + modifier + // Merge semantics at the beginning of the modifier chain to ensure padding is + // considered part of the text field. + .semantics(mergeDescendants = true) {} + .padding(top = 8.dp) + } else { + modifier + }, +// .defaultMinSize( +// minWidth = OutlinedTextFieldDefaults.MinWidth, +// minHeight = OutlinedTextFieldDefaults.MinHeight +// ), + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + textStyle = mergedTextStyle, +// cursorBrush = SolidColor(colors.cursorColor(isError).value), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.DecorationBox( + value = value, + visualTransformation = visualTransformation, + innerTextField = innerTextField, + placeholder = placeholder, + label = label, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + singleLine = singleLine, + enabled = enabled, + isError = isError, + interactionSource = interactionSource, + colors = colors, + container = { + TextFieldDefaults.ContainerBox( + enabled, + isError, + interactionSource, + colors, + shape + ) + }, + contentPadding = PaddingValues( + start = 12.dp, top = 10.dp, end = 12.dp, bottom = 10.dp, + ) + ) + } + ) +// } +} \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 579f60cad..04486e892 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -119,7 +119,7 @@ Replace rule manager Add replace rule Replace rule - Replace with + Replace with Insert pinyin tone Use regular expressions diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2f23b1c01..32925ae1e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -117,7 +117,7 @@ 替換規則管理 新增替換規則 替換規則 - 替換為 + 替換為 插入拼音聲調 使用正則表達式 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2a0065d4..906af795b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,7 +120,7 @@ 替换规则管理 添加替换规则 替换规则 - 替换为 + 替换为 插入拼音声调 使用正则表达式