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 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Help import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FilterAlt 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.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.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhFilter 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.client.EhFilter.trigger
import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.FilterMode import com.hippo.ehviewer.dao.FilterMode
import com.hippo.ehviewer.databinding.DialogAddFilterBinding
import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.tools.Deferred import com.hippo.ehviewer.ui.tools.Deferred
import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.tarsin.coroutines.groupByToObserved import moe.tarsin.coroutines.groupByToObserved
@@ -62,36 +70,110 @@ import moe.tarsin.coroutines.groupByToObserved
@Composable @Composable
fun FilterScreen(navigator: DestinationsNavigator) { fun FilterScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope { Dispatchers.IO } val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } } val allFilterMap = remember { scope.async { EhFilter.filters.await().groupByToObserved { it.mode } } }
val dialogState = LocalDialogState.current val dialogState = LocalDialogState.current
class AddFilterDialogHelper(private val dialog: AlertDialog) : View.OnClickListener { val textIsEmpty = stringResource(R.string.text_is_empty)
private val mArray = dialog.context.resources.getStringArray(R.array.filter_entries) val labelExist = stringResource(R.string.label_text_exist)
private val binding = DialogAddFilterBinding.bind(dialog.findViewById(R.id.base)!!)
init { fun addFilter() {
dialog.getButton(DialogInterface.BUTTON_POSITIVE)!!.setOnClickListener(this) scope.launch {
} dialogState.dialog { cont ->
override fun onClick(v: View) { val types = stringArrayResource(id = R.array.filter_entries)
binding.textInputLayout.run { var type by remember { mutableStateOf(types[0]) }
val text = editText!!.text.toString().trim() var value by remember { mutableStateOf("") }
if (text.isBlank()) { var error by remember { mutableStateOf<String?>(null) }
error = context.getString(R.string.text_is_empty) fun invalidateAndSave() {
return if (value.isBlank()) {
} else { error = textIsEmpty
return
}
error = null error = null
} val mode = FilterMode.entries[types.indexOf(type)]
val modeText = binding.spinner.editText!!.text.toString() val filter = Filter(mode, value)
val mode = FilterMode.entries[mArray.indexOf(modeText)] filter.remember {
val filter = Filter(mode, text) if (it) {
filter.remember { cont.resume(Unit)
if (it) { requireNotNull(allFilterMap.getCompleted()[mode]).add(filter)
post { dialog.dismiss() } } else {
requireNotNull(allFilterMap.getCompleted()[mode]).add(filter) error = labelExist
} else { }
post { error = context.getString(R.string.label_text_exist) }
} }
} }
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 = {
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) Icon(imageVector = Icons.Default.Add, contentDescription = null)
} }
}, },
@@ -135,18 +217,18 @@ fun FilterScreen(navigator: DestinationsNavigator) {
FilterMode.COMMENT -> R.string.filter_comment FilterMode.COMMENT -> R.string.filter_comment
} }
if (filters.isNotEmpty()) { if (filters.isNotEmpty()) {
stickyHeader { stickyHeader(key = filterMode) {
Text( Text(
text = stringResource(id = title), 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, color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
) )
} }
items(filters) { filter -> items(filters, key = { requireNotNull(it.id) }) { filter ->
val filterCheckBoxRecomposeScope = currentRecomposeScope val filterCheckBoxRecomposeScope = currentRecomposeScope
Row( Row(
modifier = Modifier.fillMaxWidth().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } }, modifier = Modifier.fillMaxWidth().animateItemPlacement().clickable { filter.trigger { filterCheckBoxRecomposeScope.invalidate() } },
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox( Checkbox(
@@ -161,9 +243,7 @@ fun FilterScreen(navigator: DestinationsNavigator) {
Text(text = stringResource(id = R.string.delete_filter, filter.text)) Text(text = stringResource(id = R.string.delete_filter, filter.text))
} }
filter.forget { 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>