Add autocomplete tag search in filters for E/Exhentai
This commit is contained in:
parent
61e4ff548c
commit
50eef307f4
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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<*, *>
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 <--
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21989
app/src/main/java/exh/eh/EHTags.kt
Normal file
21989
app/src/main/java/exh/eh/EHTags.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@ class FilterSerializer {
|
||||
val serializers = listOf<Serializer<*>>(
|
||||
// EXH -->
|
||||
HelpDialogSerializer(this),
|
||||
AutoCompleteSerializer(this),
|
||||
// EXH <--
|
||||
HeaderSerializer(this),
|
||||
SeparatorSerializer(this),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
43
app/src/main/res/layout/navigation_view_autocomplete.xml
Normal file
43
app/src/main/res/layout/navigation_view_autocomplete.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user