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) 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is Filter<*>) return false 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.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHTags
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.eh.EHentaiUpdateWorkerConstants import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry import exh.eh.GalleryEntry
@ -280,7 +281,8 @@ class EHentai(
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> { private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon() val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
uri.appendQueryParameter("f_search", query)
uri.appendQueryParameter("f_search", (query + " " + combineQuery(filters)).trim())
filters.forEach { filters.forEach {
if (it is UriFilter) it.addToUri(uri) if (it is UriFilter) it.addToUri(uri)
} }
@ -614,7 +616,14 @@ class EHentai(
}.build() }.build()
// Filters // 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()) { if (prefs.eh_watchedListDefaultState().get()) {
Watched(isEnabled = true) Watched(isEnabled = true)
} else { } else {
@ -624,6 +633,7 @@ class EHentai(
AdvancedGroup(), AdvancedGroup(),
ReverseFilter() ReverseFilter()
) )
}
class Watched(val isEnabled: Boolean) : Filter.CheckBox("Watched List", isEnabled), UriFilter { class Watched(val isEnabled: Boolean) : Filter.CheckBox("Watched List", isEnabled), UriFilter {
override fun addToUri(builder: Uri.Builder) { 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 MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt") 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.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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.CheckboxItem
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
@ -309,6 +311,7 @@ open class BrowseSourcePresenter(
is Filter.Header -> HeaderItem(filter) is Filter.Header -> HeaderItem(filter)
// --> EXH // --> EXH
is Filter.HelpDialog -> HelpDialogItem(filter) is Filter.HelpDialog -> HelpDialogItem(filter)
is Filter.AutoComplete -> AutoComplete(filter)
// <-- EXH // <-- EXH
is Filter.Separator -> SeparatorItem(filter) is Filter.Separator -> SeparatorItem(filter)
is Filter.CheckBox -> CheckboxItem(filter) is Filter.CheckBox -> CheckboxItem(filter)
@ -323,6 +326,9 @@ open class BrowseSourcePresenter(
is Filter.TriState -> TriStateSectionItem(it) is Filter.TriState -> TriStateSectionItem(it)
is Filter.Text -> TextSectionItem(it) is Filter.Text -> TextSectionItem(it)
is Filter.Select<*> -> SelectSectionItem(it) is Filter.Select<*> -> SelectSectionItem(it)
// SY -->
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <--
else -> null else -> null
} as? ISectionable<*, *> } 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() 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<*>>( val serializers = listOf<Serializer<*>>(
// EXH --> // EXH -->
HelpDialogSerializer(this), HelpDialogSerializer(this),
AutoCompleteSerializer(this),
// EXH <-- // EXH <--
HeaderSerializer(this), HeaderSerializer(this),
SeparatorSerializer(this), SeparatorSerializer(this),

View File

@ -1,9 +1,12 @@
package xyz.nulldev.ts.api.http.serializer package xyz.nulldev.ts.api.http.serializer
import com.github.salomonbrys.kotson.bool import com.github.salomonbrys.kotson.bool
import com.github.salomonbrys.kotson.forEach
import com.github.salomonbrys.kotson.int import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.nullArray
import com.github.salomonbrys.kotson.nullObj import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.set import com.github.salomonbrys.kotson.set
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
@ -218,3 +221,33 @@ class SortSerializer(override val serializer: FilterSerializer) : Serializer<Fil
const val STATE_ASCENDING = "ascending" 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>