Refactor FilterScreen dialog to Compose, animateItemPlacement

This commit is contained in:
GG Bond authored and Vladimir Dostoyevskiy committed 2024-01-23 12:49:12 +08:00
1 parent 20b145bee5
commit ccc6d004de
2 files changed
+118 -99

No files matched your search

@@ -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<String?>(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)
}
}
},
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/base"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="?attr/dialogPreferredPadding"
android:paddingTop="@dimen/abc_dialog_padding_top_material">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/spinner"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/filter_label">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="@string/filter_title"
app:simpleItems="@array/filter_entries" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:hint="@string/filter_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="text"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>