Add autocomplete tag search in filters for E/Exhentai

This commit is contained in:
Jobobby04 2020-06-30 13:36:14 -04:00
parent 61e4ff548c
commit 50eef307f4
10 changed files with 22352 additions and 11 deletions

View File

@ -29,6 +29,8 @@ sealed class Filter<T>(val name: String, var state: T) {
data class Selection(val index: Int, val ascending: Boolean)
}
abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Filter<*>) return false

View File

@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.debug.DebugToggles
import exh.eh.EHTags
import exh.eh.EHentaiUpdateHelper
import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry
@ -280,7 +281,8 @@ class EHentai(
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
uri.appendQueryParameter("f_search", query)
uri.appendQueryParameter("f_search", (query + " " + combineQuery(filters)).trim())
filters.forEach {
if (it is UriFilter) it.addToUri(uri)
}
@ -614,7 +616,14 @@ class EHentai(
}.build()
// Filters
override fun getFilterList() = FilterList(
override fun getFilterList(): FilterList {
val excludePrefix = "-"
return FilterList(
AutoCompleteTags(
EHTags.getNameSpaces().map { "$it:" } + EHTags.getAllTags(),
EHTags.getNameSpaces().map { "$it:" }, excludePrefix
),
if (prefs.eh_watchedListDefaultState().get()) {
Watched(isEnabled = true)
} else {
@ -624,6 +633,7 @@ class EHentai(
AdvancedGroup(),
ReverseFilter()
)
}
class Watched(val isEnabled: Boolean) : Filter.CheckBox("Watched List", isEnabled), UriFilter {
override fun addToUri(builder: Uri.Builder) {
@ -679,6 +689,41 @@ class EHentai(
}
}
private fun combineQuery(filters: FilterList): String {
val stringBuilder = StringBuilder()
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
val splitState = filter.state.map(String::trim).filterNot(String::isBlank)
splitState.mapNotNull { tag ->
val split = tag.split(":").filterNot { it.isBlank() }.toMutableList()
if (split.size > 1) {
val namespace = split[0].removePrefix("-")
val exclude = split[0].startsWith("-")
split -= namespace
AdvSearchEntry(Pair(namespace, split.joinToString(":")), exclude)
} else {
null
}
}
}
advSearch.forEach { entry ->
if (entry.exclude) stringBuilder.append("-")
if (entry.search.second.contains(" ")) {
stringBuilder.append(("${entry.search.first}:\"${entry.search.second}$\""))
} else {
stringBuilder.append("${entry.search.first}:${entry.search.second}$")
}
stringBuilder.append(" ")
}
XLog.d(stringBuilder.toString())
return stringBuilder.toString().trim()
}
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean)
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : Filter.AutoComplete(name = "Tags", hint = "Search tags here (limit of 8)", values = tags, skipAutoFillTags = skipAutoFillTags, excludePrefix = excludePrefix, state = emptyList())
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")

View File

@ -20,6 +20,8 @@ import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
@ -309,6 +311,7 @@ open class BrowseSourcePresenter(
is Filter.Header -> HeaderItem(filter)
// --> EXH
is Filter.HelpDialog -> HelpDialogItem(filter)
is Filter.AutoComplete -> AutoComplete(filter)
// <-- EXH
is Filter.Separator -> SeparatorItem(filter)
is Filter.CheckBox -> CheckboxItem(filter)
@ -323,6 +326,9 @@ open class BrowseSourcePresenter(
is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it)
// SY -->
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <--
else -> null
} as? ISectionable<*, *>
}

View File

@ -0,0 +1,130 @@
package eu.kanade.tachiyomi.ui.browse.source.filter
import android.annotation.SuppressLint
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
import timber.log.Timber
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
override fun getLayoutRes(): Int {
return R.layout.navigation_view_autocomplete
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
@SuppressLint("SetTextI18n")
holder.text.text = "${filter.name}: "
holder.autoComplete.hint = filter.hint
holder.autoComplete.setAdapter(
AutoCompleteAdapter(
holder.itemView.context,
android.R.layout.simple_dropdown_item_1line,
filter.values,
filter.excludePrefix
)
)
holder.autoComplete.threshold = 3
var text: String = ""
// select from auto complete
holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ ->
val name = adapterView.getItemAtPosition(chipPosition) as String
if (name !in if (filter.excludePrefix != null && name.startsWith(filter.excludePrefix)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
holder.autoComplete.text = null
addTag(name, holder)
}
}
// done keyboard button is pressed
holder.autoComplete.setOnEditorActionListener { textView, actionId, keyEvent ->
if (actionId == EditorInfo.IME_ACTION_DONE && textView.text.toString() !in if (filter.excludePrefix != null && textView.text.toString().startsWith(filter.excludePrefix)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) {
textView.text = null
addTag(textView.text.toString(), holder)
return@setOnEditorActionListener true
}
false
}
// space or comma is detected
holder.autoComplete.addTextChangedListener {
if (it == null || it.isEmpty()) {
return@addTextChangedListener
}
text = it.toString()
if (it.last() == ',') {
val name = it.substring(0, it.length - 1)
addTag(name, holder)
holder.autoComplete.text = null
// mainTagAutoCompleteTextView.removeTextChangedListener(this)
}
}
holder.mainTagChipGroup.removeAllViews()
filter.state.forEach {
addChipToGroup(it, holder)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return filter == (other as SelectItem).filter
}
override fun hashCode(): Int {
return filter.hashCode()
}
private fun addTag(name: String, holder: Holder) {
if (name.isNotEmpty() && !filter.state.contains(name)) {
addChipToGroup(name, holder)
filter.state += name
} else {
Timber.d("Invalid tag: $name")
}
}
private fun addChipToGroup(name: String, holder: Holder) {
val chip = Chip(holder.itemView.context)
chip.text = name
chip.isClickable = true
chip.isCheckable = false
chip.isCloseIconVisible = true
holder.mainTagChipGroup.addView(chip)
chip.setOnCloseIconClickListener {
holder.mainTagChipGroup.removeView(chip)
filter.state -= name
}
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
val autoComplete: AutoCompleteTextView = itemView.findViewById(R.id.nav_view_item)
val mainTagChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group)
}
}

View File

@ -86,3 +86,26 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection
return filter.hashCode()
}
}
// SY -->
class AutoCompleteSectionItem(filter: Filter.AutoComplete) : AutoComplete(filter), ISectionable<AutoComplete.Holder, GroupItem> {
private var head: GroupItem? = null
override fun getHeader(): GroupItem? = head
override fun setHeader(header: GroupItem?) {
head = header
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return filter == (other as AutoCompleteSectionItem).filter
}
override fun hashCode(): Int {
return filter.hashCode()
}
}
// SY <--

View File

@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.Filterable
import timber.log.Timber
class AutoCompleteAdapter(context: Context, resource: Int, var objects: List<String>, val excludePrefix: String?) :
ArrayAdapter<String>(context, resource, objects),
Filterable {
private var mOriginalValues: List<String>? = objects
private var mFilter: ListFilter? = null
override fun getCount(): Int {
return objects.size
}
override fun getItem(position: Int): String {
return objects[position]
}
override fun getFilter(): Filter {
if (mFilter == null) {
mFilter = ListFilter()
}
return mFilter!!
}
private inner class ListFilter : Filter() {
override fun performFiltering(prefix: CharSequence?): FilterResults {
val results = FilterResults()
if (mOriginalValues == null) {
mOriginalValues = objects
}
Timber.d("$prefix ")
if (prefix == null || prefix.isEmpty()) {
val list = mOriginalValues!!
results.values = list
results.count = list.size
} else {
val prefixString = prefix.toString()
val containsPrefix: Boolean = excludePrefix?.let { prefixString.startsWith(it) } ?: false
Timber.d(prefixString)
val filterResults = mOriginalValues!!.filter { it.contains(if (excludePrefix != null) prefixString.removePrefix(excludePrefix) else prefixString, true) }
results.values = if (containsPrefix) filterResults.map { excludePrefix + it } else filterResults
results.count = filterResults.size
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
objects = if (results.values != null) {
results.values as List<String>? ?: emptyList()
} else {
emptyList()
}
if (results.count > 0) {
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ class FilterSerializer {
val serializers = listOf<Serializer<*>>(
// EXH -->
HelpDialogSerializer(this),
AutoCompleteSerializer(this),
// EXH <--
HeaderSerializer(this),
SeparatorSerializer(this),

View File

@ -1,9 +1,12 @@
package xyz.nulldev.ts.api.http.serializer
import com.github.salomonbrys.kotson.bool
import com.github.salomonbrys.kotson.forEach
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.nullArray
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.set
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray
import com.google.gson.JsonNull
import com.google.gson.JsonObject
@ -218,3 +221,33 @@ class SortSerializer(override val serializer: FilterSerializer) : Serializer<Fil
const val STATE_ASCENDING = "ascending"
}
}
class AutoCompleteSerializer(override val serializer: FilterSerializer) : Serializer<Filter.AutoComplete> {
override val type = "AUTOCOMPLETE"
override val clazz = Filter.AutoComplete::class
override fun serialize(json: JsonObject, filter: Filter.AutoComplete) {
// Serialize values to JSON
json[STATE] = JsonArray().apply {
filter.state.forEach { add(it) }
}
}
override fun deserialize(json: JsonObject, filter: Filter.AutoComplete) {
// Deserialize state
json[STATE].nullArray?.let { array ->
filter.state = array.map {
it.string
}
}
}
override fun mappings() = listOf(
Pair(NAME, Filter.AutoComplete::name)
)
companion object {
const val NAME = "name"
const val STATE = "state"
}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall">
<TextView
android:id="@+id/nav_view_item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="Filter:" />
<AutoCompleteTextView
android:id="@+id/nav_view_item"
style="@style/Theme.Widget.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:completionThreshold="1"
android:gravity="center_vertical|start"
android:imeOptions="actionDone"
android:inputType="textCapWords" />
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="4dp"
style="@style/Theme.Widget.Chip"/>
</LinearLayout>