From ccc6d004de30b4f5731fef9ce6bf15e4a975e5b3 Mon Sep 17 00:00:00 2001 From: GG Bond Date: Tue, 23 Jan 2024 11:07:41 +0800 Subject: [PATCH] Refactor FilterScreen dialog to Compose, animateItemPlacement --- .../ehviewer/ui/settings/FilterScreen.kt | 156 +++++++++++++----- app/src/main/res/layout/dialog_add_filter.xml | 61 ------- 2 files changed, 118 insertions(+), 99 deletions(-) delete mode 100644 app/src/main/res/layout/dialog_add_filter.xml 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 752522889..31a3c5ed9 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 @@ -1,8 +1,5 @@ package com.hippo.ehviewer.ui.settings -import android.content.DialogInterface -import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,30 +11,43 @@ import androidx.compose.foundation.layout.padding 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.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Help import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.currentRecomposeScope +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.hippo.ehviewer.R import com.hippo.ehviewer.client.EhFilter @@ -46,14 +56,12 @@ import com.hippo.ehviewer.client.EhFilter.remember import com.hippo.ehviewer.client.EhFilter.trigger import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.FilterMode -import com.hippo.ehviewer.databinding.DialogAddFilterBinding import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder import com.hippo.ehviewer.ui.tools.Deferred import com.hippo.ehviewer.ui.tools.LocalDialogState import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import eu.kanade.tachiyomi.util.lang.withUIContext -import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.resume import kotlinx.coroutines.async import kotlinx.coroutines.launch import moe.tarsin.coroutines.groupByToObserved @@ -62,36 +70,110 @@ import moe.tarsin.coroutines.groupByToObserved @Composable fun FilterScreen(navigator: DestinationsNavigator) { val context = LocalContext.current - val scope = rememberCoroutineScope { Dispatchers.IO } + val scope = rememberCoroutineScope() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } } val dialogState = LocalDialogState.current - class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener { - private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries) - private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!) - init { - dialog.getButton(DialogInterface.BUTTON_POSITIVE)!!.setOnClickListener(this) - } - override fun onClick(v: View) { - binding.textInputLayout.run { - val text = editText!!.text.toString().trim() - if (text.isBlank()) { - error = context.getString(R.string.text_is_empty) - return - } else { + val textIsEmpty = stringResource(R.string.text_is_empty) + val labelExist = stringResource(R.string.label_text_exist) + + fun addFilter() { + 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("") } + var error by remember { mutableStateOf(null) } + fun invalidateAndSave() { + if (value.isBlank()) { + error = textIsEmpty + return + } error = null - } - val modeText = binding.spinner.editText!!.text.toString() - val mode = FilterMode.entries[mArray.indexOf(modeText)] - val filter = Filter(mode, text) - filter.remember { - if (it) { - post { dialog.dismiss() } - requireNotNull(allFilterMap.getCompleted()[mode]).add(filter) - } else { - post { error = context.getString(R.string.label_text_exist) } + val mode = FilterMode.entries[types.indexOf(type)] + val filter = Filter(mode, value) + filter.remember { + if (it) { + cont.resume(Unit) + requireNotNull(allFilterMap.getCompleted()[mode]).add(filter) + } else { + error = labelExist + } } } + AlertDialog( + onDismissRequest = { cont.cancel() }, + confirmButton = { + TextButton(onClick = ::invalidateAndSave) { + Text(text = stringResource(id = R.string.add)) + } + }, + dismissButton = { + TextButton(onClick = { cont.cancel() }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + title = { + Text(text = stringResource(id = R.string.add_filter)) + }, + text = { + var expanded by remember { mutableStateOf(false) } + Column { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + OutlinedTextField( + modifier = Modifier.menuAnchor(), + readOnly = true, + value = type, + onValueChange = {}, + label = { + Text(text = stringResource(id = R.string.filter_label)) + }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + types.forEach { + DropdownMenuItem( + text = { Text(text = it) }, + onClick = { + expanded = false + type = it + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + } + Spacer(modifier = Modifier.size(16.dp)) + val isError = error != null + OutlinedTextField( + value = value, + onValueChange = { value = it }, + label = { Text(text = stringResource(id = R.string.filter_text)) }, + supportingText = { error?.let { Text(text = it) } }, + trailingIcon = { + if (isError) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + ) + } + }, + isError = isError, + maxLines = 1, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + ) + } + }, + ) } } } @@ -114,7 +196,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { ) }, floatingActionButton = { - FloatingActionButton(onClick = { AddFilterDialogHelper(BaseDialogBuilder(context).setTitle(R.string.add_filter).setView(R.layout.dialog_add_filter).setPositiveButton(R.string.add, null).setNegativeButton(android.R.string.cancel, null).show()) }) { + FloatingActionButton(onClick = ::addFilter) { Icon(imageVector = Icons.Default.Add, contentDescription = null) } }, @@ -135,18 +217,18 @@ fun FilterScreen(navigator: DestinationsNavigator) { FilterMode.COMMENT -> R.string.filter_comment } if (filters.isNotEmpty()) { - stickyHeader { + stickyHeader(key = filterMode) { Text( text = stringResource(id = title), - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp).animateItemPlacement(), color = MaterialTheme.colorScheme.tertiary, style = MaterialTheme.typography.titleMedium, ) } - items(filters) { filter -> + items(filters, key = { requireNotNull(it.id) }) { filter -> val filterCheckBoxRecomposeScope = currentRecomposeScope Row( - modifier = Modifier.fillMaxWidth().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } }, + modifier = Modifier.fillMaxWidth().animateItemPlacement().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } }, verticalAlignment = Alignment.CenterVertically, ) { Checkbox( @@ -161,9 +243,7 @@ fun FilterScreen(navigator: DestinationsNavigator) { Text(text = stringResource(id = R.string.delete_filter, filter.text)) } filter.forget { - withUIContext { - filters.remove(filter) - } + filters.remove(filter) } } }, diff --git a/app/src/main/res/layout/dialog_add_filter.xml b/app/src/main/res/layout/dialog_add_filter.xml deleted file mode 100644 index 487c869bf..000000000 --- a/app/src/main/res/layout/dialog_add_filter.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - -