diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AlertDialogs.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AlertDialogs.kt index dd3fe5a88..178f7c8f9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AlertDialogs.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/AlertDialogs.kt @@ -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.setEditText( setView(layout) return editText } + +fun B.setEditText( + entries: List, + 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 +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/PersistableFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/PersistableFilter.kt index 12e6093cb..c21211032 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/PersistableFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/PersistableFilter.kt @@ -21,7 +21,7 @@ data class PersistableFilter( ) { val id: Int - get() = filter.hashCode() + get() = name.hashCode() companion object { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/SavedFiltersRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/SavedFiltersRepository.kt index c5300be2d..a1cf9fafb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/SavedFiltersRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/data/SavedFiltersRepository.kt @@ -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(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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt index 6a67926af..51e58ea20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index 336f9c9f5..0e34cb1b4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -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(), AdapterView.OnItemSelectedListener, @@ -110,9 +113,6 @@ class FilterSheetFragment : BaseAdaptiveSheet(), 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(), 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(), 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(), 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(), 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(), 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() + } } diff --git a/app/src/main/res/layout/preference_dialog_autocompletetextview.xml b/app/src/main/res/layout/preference_dialog_autocompletetextview.xml index 38f92332d..952e835ef 100644 --- a/app/src/main/res/layout/preference_dialog_autocompletetextview.xml +++ b/app/src/main/res/layout/preference_dialog_autocompletetextview.xml @@ -42,7 +42,7 @@ android:importantForAutofill="no" android:minHeight="48dp" /> - + android:text="@string/done" /> diff --git a/app/src/main/res/layout/view_dialog_autocomplete.xml b/app/src/main/res/layout/view_dialog_autocomplete.xml new file mode 100644 index 000000000..d8ceaaa5c --- /dev/null +++ b/app/src/main/res/layout/view_dialog_autocomplete.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 552c3935b..d85322073 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -893,4 +893,6 @@ Test manga source Rename Save filter + Overwrite + A filter named \"%s\" already exists. Do you want to overwrite it?