From c92bdae842b5a68a1f3490830c61b511e1b51914 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 8 Apr 2022 14:56:45 +0300 Subject: [PATCH] Add tags blacklist option for suggestions --- .../kotatsu/core/prefs/AppSettings.kt | 13 ++ .../settings/SuggestionsSettingsFragment.kt | 8 ++ .../MultiAutoCompleteTextViewPreference.kt | 118 ++++++++++++++++++ .../utils/TagsAutoCompleteProvider.kt | 23 ++++ .../suggestions/ui/SuggestionsWorker.kt | 10 +- ...rence_dialog_multiautocompletetextview.xml | 42 +++++++ ...{pref_slider.xml => preference_slider.xml} | 0 app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 6 +- app/src/main/res/xml/pref_suggestions.xml | 6 + 12 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt create mode 100644 app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml rename app/src/main/res/layout/{pref_slider.xml => preference_slider.xml} (100%) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ac199619b..de191e304 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -165,6 +165,18 @@ class AppSettings(context: Context) { else -> SimpleDateFormat(format, Locale.getDefault()) } + fun getSuggestionsTagsBlacklistRegex(): Regex? { + val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') + if (string.isNullOrEmpty()) { + return null + } + val tags = string.split(',') + val regex = tags.joinToString(prefix = "(", separator = "|", postfix = ")") { tag -> + Regex.escape(tag.trim()) + } + return Regex(regex, RegexOption.IGNORE_CASE) + } + fun getMangaSources(includeHidden: Boolean): List { val list = MangaSource.values().toMutableList() list.remove(MangaSource.LOCAL) @@ -247,6 +259,7 @@ class AppSettings(context: Context) { const val KEY_PAGES_PRELOAD = "pages_preload" const val KEY_SUGGESTIONS = "suggestions" const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw" + const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags" const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source" // About diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt index 02467f1d6..46aced8a0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt @@ -4,10 +4,13 @@ import android.content.SharedPreferences import android.os.Bundle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.utils.MultiAutoCompleteTextViewPreference +import org.koitharu.kotatsu.settings.utils.TagsAutoCompleteProvider import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker @@ -23,6 +26,11 @@ class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_suggestions) + + findPreference(AppSettings.KEY_SUGGESTIONS_EXCLUDE_TAGS)?.run { + autoCompleteProvider = TagsAutoCompleteProvider(get()) + summaryProvider = MultiAutoCompleteTextViewPreference.SimpleSummaryProvider(summary) + } } override fun onDestroy() { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt new file mode 100644 index 000000000..bccaaa942 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/MultiAutoCompleteTextViewPreference.kt @@ -0,0 +1,118 @@ +package org.koitharu.kotatsu.settings.utils + +import android.content.Context +import android.util.AttributeSet +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Filter +import android.widget.MultiAutoCompleteTextView +import androidx.annotation.AttrRes +import androidx.annotation.MainThread +import androidx.annotation.StyleRes +import androidx.annotation.WorkerThread +import androidx.preference.EditTextPreference +import kotlinx.coroutines.runBlocking +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.parsers.util.replaceWith + +class MultiAutoCompleteTextViewPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = R.attr.multiAutoCompleteTextViewPreferenceStyle, + @StyleRes defStyleRes: Int = R.style.Preference_MultiAutoCompleteTextView, +) : EditTextPreference(context, attrs, defStyleAttr, defStyleRes) { + + private val autoCompleteBindListener = AutoCompleteBindListener() + + var autoCompleteProvider: AutoCompleteProvider? = null + + init { + super.setOnBindEditTextListener(autoCompleteBindListener) + } + + override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) { + autoCompleteBindListener.delegate = onBindEditTextListener + } + + private inner class AutoCompleteBindListener : OnBindEditTextListener { + + var delegate: OnBindEditTextListener? = null + + override fun onBindEditText(editText: EditText) { + delegate?.onBindEditText(editText) + if (editText !is MultiAutoCompleteTextView) { + return + } + editText.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer()) + editText.setAdapter( + autoCompleteProvider?.let { + CompletionAdapter(editText.context, it, ArrayList()) + } + ) + editText.threshold = 1 + } + } + + interface AutoCompleteProvider { + + suspend fun getSuggestions(query: String): List + } + + class SimpleSummaryProvider( + private val emptySummary: CharSequence?, + ) : SummaryProvider { + + override fun provideSummary(preference: MultiAutoCompleteTextViewPreference): CharSequence? { + return if (preference.text.isNullOrEmpty()) { + emptySummary + } else { + preference.text?.trimEnd(' ', ',') + } + } + } + + private class CompletionAdapter( + context: Context, + private val completionProvider: AutoCompleteProvider, + private val dataset: MutableList, + ) : ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, dataset) { + + override fun getFilter(): Filter { + return CompletionFilter(this, completionProvider) + } + + fun publishResults(results: List) { + dataset.replaceWith(results) + notifyDataSetChanged() + } + } + + private class CompletionFilter( + private val adapter: CompletionAdapter, + private val provider: AutoCompleteProvider, + ) : Filter() { + + @WorkerThread + override fun performFiltering(constraint: CharSequence?): FilterResults { + val query = constraint?.toString().orEmpty() + val suggestions = runBlocking { provider.getSuggestions(query) } + return CompletionResults(suggestions) + } + + @MainThread + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + val completions = (results as CompletionResults).completions + adapter.publishResults(completions) + } + + private class CompletionResults( + val completions: List, + ) : FilterResults() { + + init { + values = completions + count = completions.size + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt new file mode 100644 index 000000000..53998e48b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/TagsAutoCompleteProvider.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.settings.utils + +import org.koitharu.kotatsu.core.db.MangaDatabase + +class TagsAutoCompleteProvider( + private val db: MangaDatabase, +) : MultiAutoCompleteTextViewPreference.AutoCompleteProvider { + + override suspend fun getSuggestions(query: String): List { + if (query.isEmpty()) { + return emptyList() + } + val tags = db.tagsDao.findTags(query = "$query%", limit = 6) + val set = HashSet() + val result = ArrayList(tags.size) + for (tag in tags) { + if (set.add(tag.title)) { + result.add(tag.title) + } + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 46bb42b5a..7433e689b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -76,7 +76,10 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : suggestionRepository.clear() return 0 } - val allTags = historyRepository.getPopularTags(TAGS_LIMIT) + val blacklistTagRegex = appSettings.getSuggestionsTagsBlacklistRegex() + val allTags = historyRepository.getPopularTags(TAGS_LIMIT).filterNot { + blacklistTagRegex?.containsMatchIn(it.title) ?: false + } if (allTags.isEmpty()) { return 0 } @@ -102,6 +105,11 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : if (appSettings.isSuggestionsExcludeNsfw) { rawResults.removeAll { it.isNsfw } } + if (blacklistTagRegex != null) { + rawResults.removeAll { + it.tags.any { x -> blacklistTagRegex.containsMatchIn(x.title) } + } + } if (rawResults.isEmpty()) { return 0 } diff --git a/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml b/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml new file mode 100644 index 000000000..bcf067dc3 --- /dev/null +++ b/app/src/main/res/layout/preference_dialog_multiautocompletetextview.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_slider.xml b/app/src/main/res/layout/preference_slider.xml similarity index 100% rename from app/src/main/res/layout/pref_slider.xml rename to app/src/main/res/layout/preference_slider.xml diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1fc097e9e..6a432f53a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -265,4 +265,7 @@ В этой манге нет глав Оформление Контент + Обновление рекомендаций + Исключить жанры + Укажите жанры, которые Вы не хотите видеть в рекомендациях \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 42d3174b2..449fc3a29 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a880bd56..9a4552a1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,4 +269,6 @@ GitHub Discord Suggestions updating + Exclude genres + Specify genres that you do not want to see in the suggestions \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f0bdca57b..37da1ddcb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -129,7 +129,11 @@ + + diff --git a/app/src/main/res/xml/pref_suggestions.xml b/app/src/main/res/xml/pref_suggestions.xml index 0206aa4c2..224185a67 100644 --- a/app/src/main/res/xml/pref_suggestions.xml +++ b/app/src/main/res/xml/pref_suggestions.xml @@ -9,6 +9,12 @@ android:summary="@string/suggestions_summary" android:title="@string/suggestions_enable" /> + +