Improve saved filters
This commit is contained in:
@@ -5,6 +5,7 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
@@ -21,6 +22,7 @@ import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager
|
||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
import org.koitharu.kotatsu.databinding.ViewDialogAutocompleteBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
inline fun buildAlertDialog(
|
||||
@@ -96,3 +98,27 @@ fun <B : AlertDialog.Builder> B.setEditText(
|
||||
setView(layout)
|
||||
return editText
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setEditText(
|
||||
entries: List<CharSequence>,
|
||||
inputType: Int,
|
||||
singleLine: Boolean,
|
||||
): EditText {
|
||||
if (entries.isEmpty()) {
|
||||
return setEditText(inputType, singleLine)
|
||||
}
|
||||
val binding = ViewDialogAutocompleteBinding.inflate(LayoutInflater.from(context))
|
||||
binding.autoCompleteTextView.setAdapter(
|
||||
ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, entries),
|
||||
)
|
||||
binding.dropdown.setOnClickListener {
|
||||
binding.autoCompleteTextView.showDropDown()
|
||||
}
|
||||
binding.autoCompleteTextView.inputType = inputType
|
||||
if (singleLine) {
|
||||
binding.autoCompleteTextView.setSingleLine()
|
||||
binding.autoCompleteTextView.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
setView(binding.root)
|
||||
return binding.autoCompleteTextView
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ data class PersistableFilter(
|
||||
) {
|
||||
|
||||
val id: Int
|
||||
get() = filter.hashCode()
|
||||
get() = name.hashCode()
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -61,13 +61,19 @@ class SavedFiltersRepository @Inject constructor(
|
||||
|
||||
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
|
||||
val filter = load(source, id) ?: return@withContext
|
||||
persist(source, filter.copy(name = newName))
|
||||
val newFilter = filter.copy(name = newName)
|
||||
val prefs = getPrefs(source)
|
||||
prefs.edit(commit = true) {
|
||||
remove(key(id))
|
||||
putString(key(newFilter.id), Json.encodeToString(newFilter))
|
||||
}
|
||||
newFilter
|
||||
}
|
||||
|
||||
suspend fun delete(source: MangaSource, id: Int) = withContext(Dispatchers.Default) {
|
||||
val prefs = getPrefs(source)
|
||||
prefs.edit(commit = true) {
|
||||
remove(FILTER_PREFIX + id)
|
||||
remove(key(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,13 +81,13 @@ class SavedFiltersRepository @Inject constructor(
|
||||
val prefs = getPrefs(source)
|
||||
val json = Json.encodeToString(persistableFilter)
|
||||
prefs.edit(commit = true) {
|
||||
putString(FILTER_PREFIX + persistableFilter.id, json)
|
||||
putString(key(persistableFilter.id), json)
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(source: MangaSource, id: Int): PersistableFilter? {
|
||||
val prefs = getPrefs(source)
|
||||
val json = prefs.getString(FILTER_PREFIX + id, null) ?: return null
|
||||
val json = prefs.getString(key(id), null) ?: return null
|
||||
return try {
|
||||
Json.decodeFromString<PersistableFilter>(json)
|
||||
} catch (e: SerializationException) {
|
||||
@@ -95,5 +101,7 @@ class SavedFiltersRepository @Inject constructor(
|
||||
private companion object {
|
||||
|
||||
const val FILTER_PREFIX = "__pf_"
|
||||
|
||||
fun key(id: Int) = FILTER_PREFIX + id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ class FilterHeaderProducer @Inject constructor(
|
||||
data = saved,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
selectedTags.removeAll(saved.filter.tags)
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
@@ -16,7 +17,6 @@ import androidx.core.view.updatePadding
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -30,6 +30,7 @@ import org.koitharu.kotatsu.core.ui.dialog.setEditText
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.consume
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
@@ -49,8 +50,10 @@ import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import java.util.Locale
|
||||
import java.util.TreeSet
|
||||
|
||||
class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
AdapterView.OnItemSelectedListener,
|
||||
@@ -110,9 +113,6 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
binding.layoutGenresExclude.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = true)
|
||||
}
|
||||
filter.observe().observe(viewLifecycleOwner) {
|
||||
binding.buttonReset.isEnabled = it.listFilter.isNotEmpty()
|
||||
}
|
||||
combine(
|
||||
filter.observe().map { it.listFilter.isNotEmpty() }.distinctUntilChanged(),
|
||||
filter.savedFilters.map { it.selectedItems.isEmpty() }.distinctUntilChanged(),
|
||||
@@ -122,7 +122,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
binding.buttonSave.isEnabled = it
|
||||
}
|
||||
binding.buttonSave.setOnClickListener(this)
|
||||
binding.buttonReset.setOnClickListener(this)
|
||||
binding.buttonDone.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
@@ -135,8 +135,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_reset -> FilterCoordinator.require(this).reset()
|
||||
R.id.button_save -> onSaveFilterClick()
|
||||
R.id.button_done -> dismiss()
|
||||
R.id.button_save -> onSaveFilterClick("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,26 +446,29 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
menu.show()
|
||||
}
|
||||
|
||||
private fun onSaveFilterClick() {
|
||||
private fun onSaveFilterClick(name: String) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
val existingNames = filter.savedFilters.value.availableItems
|
||||
.mapTo(TreeSet(AlphanumComparator()), PersistableFilter::name)
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
entries = existingNames.toList(),
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
singleLine = true,
|
||||
)
|
||||
input.setHint(R.string.enter_name)
|
||||
input.setText(name)
|
||||
input.filters += InputFilter.LengthFilter(MAX_TITLE_LENGTH)
|
||||
setTitle(R.string.save_filter)
|
||||
setPositiveButton(R.string.save) { d, _ ->
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) {
|
||||
filter.saveCurrentFilter(text)
|
||||
if (text.isNullOrEmpty()) {
|
||||
Toast.makeText(context, R.string.invalid_value_message, Toast.LENGTH_SHORT).show()
|
||||
onSaveFilterClick("")
|
||||
} else if (text in existingNames) {
|
||||
askForFilterOverwrite(filter, text)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
viewBinding?.scrollView ?: return@setPositiveButton,
|
||||
R.string.invalid_value_message,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
filter.saveCurrentFilter(text)
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
@@ -474,6 +477,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
|
||||
private fun onRenameFilterClick(preset: PersistableFilter) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
val existingNames = filter.savedFilters.value.availableItems.mapToSet { it.name }
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
@@ -485,17 +489,26 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
setTitle(R.string.rename)
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (!text.isNullOrEmpty()) {
|
||||
filter.renameSavedFilter(preset.id, text)
|
||||
if (text.isNullOrEmpty() || text in existingNames) {
|
||||
Toast.makeText(context, R.string.invalid_value_message, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Snackbar.make(
|
||||
viewBinding?.scrollView ?: return@setPositiveButton,
|
||||
R.string.invalid_value_message,
|
||||
Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
filter.renameSavedFilter(preset.id, text)
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun askForFilterOverwrite(filter: FilterCoordinator, name: String) {
|
||||
buildAlertDialog(context ?: return) {
|
||||
setTitle(R.string.save_filter)
|
||||
setMessage(getString(R.string.filter_overwrite_confirm, name))
|
||||
setPositiveButton(R.string.overwrite) { _, _ ->
|
||||
filter.saveCurrentFilter(name)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
onSaveFilterClick(name)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
android:importantForAutofill="no"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<ImageView
|
||||
<ImageButton
|
||||
android:id="@+id/dropdown"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
|
||||
@@ -286,26 +286,24 @@
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_reset"
|
||||
android:id="@+id/button_save"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="@string/reset"
|
||||
android:text="@string/save"
|
||||
tools:enabled="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_save"
|
||||
android:id="@+id/button_done"
|
||||
style="?materialButtonTonalStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="@string/save"
|
||||
tools:enabled="true"
|
||||
tools:ignore="ButtonStyle" />
|
||||
android:text="@string/done" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
|
||||
|
||||
33
app/src/main/res/layout/view_dialog_autocomplete.xml
Normal file
33
app/src/main/res/layout/view_dialog_autocomplete.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:paddingTop="@dimen/margin_small">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/autoCompleteTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/dropdown"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/dropdown"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:paddingBottom="2dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -893,4 +893,6 @@
|
||||
<string name="test_parser">Test manga source</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="save_filter">Save filter</string>
|
||||
<string name="overwrite">Overwrite</string>
|
||||
<string name="filter_overwrite_confirm">A filter named \"%s\" already exists. Do you want to overwrite it?</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user