Add tags blacklist option for suggestions

This commit is contained in:
Koitharu
2022-04-08 14:56:45 +03:00
parent 6ca9608a80
commit c92bdae842
12 changed files with 230 additions and 2 deletions

View File

@@ -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<MangaSource> {
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

View File

@@ -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<MultiAutoCompleteTextViewPreference>(AppSettings.KEY_SUGGESTIONS_EXCLUDE_TAGS)?.run {
autoCompleteProvider = TagsAutoCompleteProvider(get())
summaryProvider = MultiAutoCompleteTextViewPreference.SimpleSummaryProvider(summary)
}
}
override fun onDestroy() {

View File

@@ -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<String>
}
class SimpleSummaryProvider(
private val emptySummary: CharSequence?,
) : SummaryProvider<MultiAutoCompleteTextViewPreference> {
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<String>,
) : ArrayAdapter<String>(context, android.R.layout.simple_dropdown_item_1line, dataset) {
override fun getFilter(): Filter {
return CompletionFilter(this, completionProvider)
}
fun publishResults(results: List<String>) {
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<String>,
) : FilterResults() {
init {
values = completions
count = completions.size
}
}
}
}

View File

@@ -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<String> {
if (query.isEmpty()) {
return emptyList()
}
val tags = db.tagsDao.findTags(query = "$query%", limit = 6)
val set = HashSet<String>()
val result = ArrayList<String>(tags.size)
for (tag in tags) {
if (set.add(tag.title)) {
result.add(tag.title)
}
}
return result
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@android:id/message"
style="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="48dp"
android:labelFor="@android:id/edit"
android:textColor="?android:attr/textColorSecondary"
tools:ignore="LabelFor" />
<MultiAutoCompleteTextView
android:id="@android:id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:importantForAutofill="no"
android:minHeight="48dp" />
</LinearLayout>
</ScrollView>

View File

@@ -265,4 +265,7 @@
<string name="chapters_empty">В этой манге нет глав</string>
<string name="appearance">Оформление</string>
<string name="content">Контент</string>
<string name="suggestions_updating">Обновление рекомендаций</string>
<string name="suggestions_excluded_genres">Исключить жанры</string>
<string name="suggestions_excluded_genres_summary">Укажите жанры, которые Вы не хотите видеть в рекомендациях</string>
</resources>

View File

@@ -2,6 +2,7 @@
<resources>
<attr name="sliderPreferenceStyle" />
<attr name="multiAutoCompleteTextViewPreferenceStyle" />
<attr name="listItemTextViewStyle" />
<declare-styleable name="Theme">

View File

@@ -269,4 +269,6 @@
<string name="github" translatable="false">GitHub</string>
<string name="discord" translatable="false">Discord</string>
<string name="suggestions_updating">Suggestions updating</string>
<string name="suggestions_excluded_genres">Exclude genres</string>
<string name="suggestions_excluded_genres_summary">Specify genres that you do not want to see in the suggestions</string>
</resources>

View File

@@ -129,7 +129,11 @@
</style>
<style name="Preference.Slider" parent="Preference.SeekBarPreference.Material">
<item name="android:layout">@layout/pref_slider</item>
<item name="android:layout">@layout/preference_slider</item>
</style>
<style name="Preference.MultiAutoCompleteTextView" parent="Preference.DialogPreference.EditTextPreference.Material">
<item name="android:dialogLayout">@layout/preference_dialog_multiautocompletetextview</item>
</style>
</resources>

View File

@@ -9,6 +9,12 @@
android:summary="@string/suggestions_summary"
android:title="@string/suggestions_enable" />
<org.koitharu.kotatsu.settings.utils.MultiAutoCompleteTextViewPreference
android:dependency="suggestions"
android:key="suggestions_exclude_tags"
android:summary="@string/suggestions_excluded_genres_summary"
android:title="@string/suggestions_excluded_genres" />
<SwitchPreferenceCompat
android:dependency="suggestions"
android:key="suggestions_exclude_nsfw"