Support new E-H/Exh search engine

This commit is contained in:
Jobobby04 2022-11-16 13:07:38 -05:00
parent eedfa44ec8
commit 797a9e6b4e
7 changed files with 210 additions and 168 deletions

View File

@ -3,68 +3,43 @@ package eu.kanade.data.source
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
abstract class EHentaiPagingSource(override val source: EHentai) : SourcePagingSource(source) { abstract class EHentaiPagingSource(override val source: EHentai) : SourcePagingSource(source) {
private var lastMangaLink: String? = null override fun getPageLoadResult(
params: LoadParams<Long>,
mangasPage: MangasPage,
): LoadResult.Page<Long, Pair<SManga, RaisedSearchMetadata?>> {
mangasPage as MetadataMangasPage
val metadata = mangasPage.mangasMetadata
abstract suspend fun fetchNextPage(currentPage: Int): MangasPage return LoadResult.Page(
data = mangasPage.mangas
override suspend fun requestNextPage(currentPage: Int): MangasPage { .mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
val lastMangaLink = lastMangaLink prevKey = null,
nextKey = mangasPage.nextKey,
val gid = if (lastMangaLink != null && source.exh) { )
EHentaiSearchMetadata.galleryId(lastMangaLink).toInt()
} else {
null
}
val mangasPage = fetchNextPage(gid ?: currentPage)
mangasPage.mangas.lastOrNull()?.let {
this.lastMangaLink = it.url
}
return if (lastMangaLink != null) {
val index = mangasPage.mangas.indexOfFirst { it.url == lastMangaLink }
if (index != -1) {
val lastIndex = mangasPage.mangas.size
val startIndex = (index + 1).coerceAtMost(mangasPage.mangas.lastIndex)
if (mangasPage is MetadataMangasPage) {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
mangasMetadata = mangasPage.mangasMetadata.subList(startIndex, lastIndex),
)
} else {
mangasPage.copy(
mangas = mangasPage.mangas.subList(startIndex, lastIndex),
)
}
} else {
mangasPage
}
} else {
mangasPage
}
} }
} }
class EHentaiSearchPagingSource(source: EHentai, val query: String, val filters: FilterList) : EHentaiPagingSource(source) { class EHentaiSearchPagingSource(source: EHentai, val query: String, val filters: FilterList) : EHentaiPagingSource(source) {
override suspend fun fetchNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchSearchManga(currentPage, query, filters).awaitSingle() return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
} }
} }
class EHentaiPopularPagingSource(source: EHentai) : EHentaiPagingSource(source) { class EHentaiPopularPagingSource(source: EHentai) : EHentaiPagingSource(source) {
override suspend fun fetchNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchPopularManga(currentPage).awaitSingle() return source.fetchPopularManga(currentPage).awaitSingle()
} }
} }
class EHentaiLatestPagingSource(source: EHentai) : EHentaiPagingSource(source) { class EHentaiLatestPagingSource(source: EHentai) : EHentaiPagingSource(source) {
override suspend fun fetchNextPage(currentPage: Int): MangasPage { override suspend fun requestNextPage(currentPage: Int): MangasPage {
return source.fetchLatestUpdates(currentPage).awaitSingle() return source.fetchLatestUpdates(currentPage).awaitSingle()
} }
} }

View File

@ -29,6 +29,16 @@ abstract class SourcePagingSource(
} catch (e: Exception) { } catch (e: Exception) {
return LoadResult.Error(e) return LoadResult.Error(e)
} }
// SY -->
return getPageLoadResult(params, mangasPage)
// SY <--
}
// SY -->
open fun getPageLoadResult(params: LoadParams<Long>, mangasPage: MangasPage): LoadResult.Page<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */> {
val page = params.key ?: 1
// SY --> // SY -->
val metadata = if (mangasPage is MetadataMangasPage) { val metadata = if (mangasPage is MetadataMangasPage) {
mangasPage.mangasMetadata mangasPage.mangasMetadata
@ -46,6 +56,7 @@ abstract class SourcePagingSource(
nextKey = if (mangasPage.hasNextPage) page + 1 else null, nextKey = if (mangasPage.hasNextPage) page + 1 else null,
) )
} }
// SY <--
override fun getRefreshKey(state: PagingState<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */>): Long? { override fun getRefreshKey(state: PagingState<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */>): Long? {
return state.anchorPosition?.let { anchorPosition -> return state.anchorPosition?.let { anchorPosition ->

View File

@ -191,14 +191,7 @@ class EHentai(
tags += parsedTags tags += parsedTags
if (infoElements != null) { if (infoElements != null) {
genre = getGenre( genre = getGenre(infoElements.getOrNull(1))
infoElements.getOrNull(1),
genreString = infoElements.getOrNull(1)
?.text()
?.nullIfBlank()
?.lowercase()
?.replace(" ", ""),
)
datePosted = getDateTag(infoElements.getOrNull(2)) datePosted = getDateTag(infoElements.getOrNull(2))
@ -208,13 +201,7 @@ class EHentai(
length = getPageCount(infoElements.getOrNull(5)) length = getPageCount(infoElements.getOrNull(5))
} else { } else {
val parsedGenre = body.selectFirst(".gl1c div") genre = getGenre(body.selectFirst(".gl1c div"))
genre = getGenre(
genreString = parsedGenre?.text()
?.nullIfBlank()
?.lowercase()
?.replace(" ", ""),
)
val info = body.selectFirst(".gl2c")!! val info = body.selectFirst(".gl2c")!!
val extraInfo = body.selectFirst(".gl4c")!! val extraInfo = body.selectFirst(".gl4c")!!
@ -239,34 +226,49 @@ class EHentai(
} }
}, },
) )
}.ifEmpty {
selectFirst(".searchwarn")?.let { throw Exception(it.text()) }
emptyList()
} }
val parsedLocation = doc.location().toHttpUrlOrNull() val parsedLocation = doc.location().toHttpUrlOrNull()
val isReversed = parsedLocation != null && parsedLocation.queryParameterNames.contains(REVERSE_PARAM)
// Add to page if required // Add to page if required
val hasNextPage = if (parsedLocation == null || val hasNextPage = if (isReversed) {
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM) select(".searchnav >div > a")
) { .any { "prev" in it.attr("href") }
select("a[onclick=return false]").last()
?.let {
it.text() == ">"
}
?: select(".searchnav >div > a")
.any { it.attr("href").contains("next") }
} else { } else {
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean() select(".searchnav >div > a")
.any { "next" in it.attr("href") }
} }
parsedMangas to hasNextPage val nextPage = if (parsedLocation?.pathSegments?.contains("toplist.php") == true) {
((parsedLocation!!.queryParameter("p")?.toLong() ?: 0) + 2).takeIf { it <= 200 }
} else if (hasNextPage) {
parsedMangas.let { if (isReversed) it.first() else it.last() }
.manga
.url
.let { EHentaiSearchMetadata.galleryId(it).toLong() }
} else {
null
}
parsedMangas.let { if (isReversed) it.reversed() else it } to nextPage
} }
private fun getGenre(element: Element? = null, genreString: String? = null): String? { private fun getGenre(element: Element?): String? {
return element?.attr("onclick") return element?.attr("onclick")
?.nullIfBlank() ?.nullIfBlank()
?.substringAfterLast('/') ?.substringAfterLast('/')
?.removeSuffix("'") ?.removeSuffix("'")
?.trim() ?.trim()
?.substringAfterLast('/') ?.substringAfterLast('/')
?.removeSuffix("'") ?: genreString ?.removeSuffix("'")
?: element?.text()
?.nullIfBlank()
?.lowercase()
?.replace(" ", "")
?.trim()
} }
private fun getDateTag(element: Element?): Long? { private fun getDateTag(element: Element?): Long? {
@ -315,8 +317,13 @@ class EHentai(
/** /**
* Parse a list of galleries * Parse a list of galleries
*/ */
private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { mangaFromSource -> private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { (parsedManga, nextPage) ->
MetadataMangasPage(mangaFromSource.first.map { it.manga }, mangaFromSource.second, mangaFromSource.first.map { it.metadata }) MetadataMangasPage(
parsedManga.map { it.manga },
nextPage != null,
parsedManga.map { it.metadata },
nextPage,
)
} }
override suspend fun getChapterList(manga: SManga): List<SChapter> = getChapterList(manga) {} override suspend fun getChapterList(manga: SManga): List<SChapter> = getChapterList(manga) {}
@ -447,7 +454,7 @@ class EHentai(
return client.newCall(chapterPageRequest(np)).asObservableSuccess() return client.newCall(chapterPageRequest(np)).asObservableSuccess()
} }
private fun chapterPageRequest(np: String): Request { private fun chapterPageRequest(np: String): Request {
return exGet(np, null, headers) return exGet(url = np, additionalHeaders = headers)
} }
private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let { private fun nextPageUrl(element: Element): String? = element.select("a[onclick=return false]").last()?.let {
@ -477,16 +484,14 @@ class EHentai(
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
searchMangaRequestObservable(page, query, filters).flatMap { super.fetchSearchManga(page, query, filters).checkValid()
client.newCall(it).asObservableSuccess()
}.map { response ->
searchMangaParse(response)
}.checkValid()
} }
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = baseUrl.toUri().buildUpon() val uri = baseUrl.toUri().buildUpon()
val toplist = ToplistOption.values()[filters.firstNotNullOfOrNull { (it as? ToplistOptions)?.state } ?: 0] val toplist = ToplistOption.values()[filters.firstNotNullOfOrNull { (it as? ToplistOptions)?.state } ?: 0]
val isReverseFilterEnabled = filters.any { it is ReverseFilter && it.state }
val jumpSeekValue = filters.firstNotNullOfOrNull { (it as? JumpSeekFilter)?.state?.nullIfBlank() }
if (toplist == ToplistOption.NONE) { if (toplist == ToplistOption.NONE) {
uri.appendQueryParameter("f_apply", "Apply+Filter") uri.appendQueryParameter("f_apply", "Apply+Filter")
@ -494,6 +499,20 @@ class EHentai(
filters.forEach { filters.forEach {
if (it is UriFilter) it.addToUri(uri) if (it is UriFilter) it.addToUri(uri)
} }
// Reverse search results on filter
if (isReverseFilterEnabled) {
uri.appendQueryParameter(REVERSE_PARAM, "on")
}
if (jumpSeekValue != null && page == 1) {
if (
MATCH_SEEK_REGEX.matches(jumpSeekValue) ||
(MATCH_YEAR_REGEX.matches(jumpSeekValue) && jumpSeekValue.toIntOrNull()?.let { it in 2007..2099 } == true)
) {
uri.appendQueryParameter("seek", jumpSeekValue)
} else if (MATCH_JUMP_REGEX.matches(jumpSeekValue)) {
uri.appendQueryParameter("jump", jumpSeekValue)
}
}
} else { } else {
uri.appendPath("toplist.php") uri.appendPath("toplist.php")
uri.appendQueryParameter("tl", toplist.index.toString()) uri.appendQueryParameter("tl", toplist.index.toString())
@ -506,52 +525,31 @@ class EHentai(
null null
} }
val request = exGet(uri.toString(), regularPage) return exGet(
url = uri.toString(),
// Reverse search results on filter next = if (!isReverseFilterEnabled) regularPage else null,
if (toplist == ToplistOption.NONE && filters.any { it is ReverseFilter && it.state }) { prev = if (isReverseFilterEnabled) regularPage else null,
return client.newCall(request) )
.asObservableSuccess()
.map {
val doc = it.asJsoup()
val elements = doc.select(".ptt > tbody > tr > td")
val totalElement = elements[elements.size - 2]
val thisPage = totalElement.text().toInt() - (page - 1)
uri.appendQueryParameter(REVERSE_PARAM, (thisPage > 1).toString())
exGet(uri.toString(), thisPage)
}
} else {
return Observable.just(request)
}
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page) override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)
override fun popularMangaParse(response: Response) = genericMangaParse(response) override fun popularMangaParse(response: Response) = genericMangaParse(response)
override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response) override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
private fun exGet(url: String, next: Int? = null, additionalHeaders: Headers? = null, cacheControl: CacheControl? = null): Request { private fun exGet(
url: String,
next: Int? = null,
prev: Int? = null,
additionalHeaders: Headers? = null,
cacheControl: CacheControl? = null,
): Request {
return GET( return GET(
if (next != null) { when {
if (exh) { next != null && next > 1 -> addParam(url, "next", next.toString())
if (next > 1) { prev != null && prev > 0 -> addParam(url, "prev", prev.toString())
addParam(url, "next", next.toString()) else -> url
} else {
url
}
} else {
addParam(url, "page", (next - 1).toString())
}
} else {
url
}, },
if (additionalHeaders != null) { if (additionalHeaders != null) {
val headers = headers.newBuilder() val headers = headers.newBuilder()
@ -815,7 +813,7 @@ class EHentai(
// Next page // Next page
page = parsed.first.lastOrNull()?.manga?.url?.let { EHentaiSearchMetadata.galleryId(it) }?.toInt() ?: 0 page = parsed.first.lastOrNull()?.manga?.url?.let { EHentaiSearchMetadata.galleryId(it) }?.toInt() ?: 0
} while (parsed.second) } while (parsed.second != null)
return Pair(result.toList(), favNames.orEmpty()) return Pair(result.toList(), favNames.orEmpty())
} }
@ -886,8 +884,6 @@ class EHentai(
// Filters // Filters
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val excludePrefix = "-"
return FilterList( return FilterList(
*if (exh) { *if (exh) {
emptyArray() emptyArray()
@ -898,19 +894,12 @@ class EHentai(
Filter.Separator(), Filter.Separator(),
) )
}, },
AutoCompleteTags( AutoCompleteTags(),
EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(), Watched(isEnabled = preferences.exhWatchedListDefaultState().get()),
EHTags.getNamespaces().map { "$it:" },
excludePrefix,
),
if (preferences.exhWatchedListDefaultState().get()) {
Watched(isEnabled = true)
} else {
Watched(isEnabled = false)
},
GenreGroup(), GenreGroup(),
AdvancedGroup(), AdvancedGroup(),
ReverseFilter(), ReverseFilter(),
JumpSeekFilter(),
) )
} }
@ -992,9 +981,16 @@ class EHentai(
filter.state.trimAll().dropBlank().mapNotNull { tag -> filter.state.trimAll().dropBlank().mapNotNull { tag ->
val split = tag.split(":").filterNot { it.isBlank() } val split = tag.split(":").filterNot { it.isBlank() }
if (split.size > 1) { if (split.size > 1) {
val namespace = split[0].removePrefix("-") val namespace = split[0].removePrefix("-").removePrefix("~")
val exclude = split[0].startsWith("-") val exclude = split[0].startsWith("-")
AdvSearchEntry(namespace to split[1], exclude) val or = split[0].startsWith("~")
AdvSearchEntry(namespace to split[1], exclude, or)
} else if (split.size == 1) {
val item = split.first()
val exclude = item.startsWith("-")
val or = item.startsWith("~")
AdvSearchEntry(null to item, exclude, or)
} else { } else {
null null
} }
@ -1003,10 +999,12 @@ class EHentai(
advSearch.forEach { entry -> advSearch.forEach { entry ->
if (entry.exclude) stringBuilder.append("-") if (entry.exclude) stringBuilder.append("-")
if (entry.or) stringBuilder.append("~")
val namespace = entry.search.first?.let { "$it:" }.orEmpty()
if (entry.search.second.contains(" ")) { if (entry.search.second.contains(" ")) {
stringBuilder.append(("""${entry.search.first}:"${entry.search.second}$"""")) stringBuilder.append(("""$namespace"${entry.search.second}$""""))
} else { } else {
stringBuilder.append("${entry.search.first}:${entry.search.second}$") stringBuilder.append("$namespace${entry.search.second}$")
} }
stringBuilder.append(" ") stringBuilder.append(" ")
} }
@ -1014,15 +1012,15 @@ class EHentai(
return stringBuilder.toString().trim().also { xLogD(it) } return stringBuilder.toString().trim().also { xLogD(it) }
} }
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean) data class AdvSearchEntry(val search: Pair<String?, String>, val exclude: Boolean, val or: Boolean)
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : class AutoCompleteTags :
Filter.AutoComplete( Filter.AutoComplete(
name = "Tags", name = "Tags",
hint = "Search tags here (limit of 8)", hint = "Search tags here (limit of 8)",
values = tags, values = EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(),
skipAutoFillTags = skipAutoFillTags, skipAutoFillTags = EHTags.getNamespaces().map { "$it:" },
excludePrefix = excludePrefix, validPrefixes = listOf("-", "~"),
state = emptyList(), state = emptyList(),
) )
@ -1052,22 +1050,21 @@ class EHentai(
class AdvancedGroup : UriGroup<Filter<*>>( class AdvancedGroup : UriGroup<Filter<*>>(
"Advanced Options", "Advanced Options",
listOf( listOf(
AdvancedOption("Search Gallery Name", "f_sname", true), AdvancedOption("Browse Expunged Galleries", "f_sh"),
AdvancedOption("Search Gallery Tags", "f_stags", true), AdvancedOption("Require Gallery Torrent", "f_sto"),
AdvancedOption("Search Gallery Description", "f_sdesc"),
AdvancedOption("Search Torrent Filenames", "f_storr"),
AdvancedOption("Only Show Galleries With Torrents", "f_sto"),
AdvancedOption("Search Low-Power Tags", "f_sdt1"),
AdvancedOption("Search Downvoted Tags", "f_sdt2"),
AdvancedOption("Show Expunged Galleries", "f_sh"),
RatingOption(), RatingOption(),
MinPagesOption(), MinPagesOption(),
MaxPagesOption(), MaxPagesOption(),
AdvancedOption("Disable custom Language filters", "f_sfl"),
AdvancedOption("Disable custom Uploader filters", "f_sfu"),
AdvancedOption("Disable custom Tag filters", "f_sft"),
), ),
) )
class ReverseFilter : Filter.CheckBox("Reverse search results") class ReverseFilter : Filter.CheckBox("Reverse search results")
class JumpSeekFilter : Filter.Text("Jump/Seek")
override val name = if (exh) { override val name = if (exh) {
"ExHentai" "ExHentai"
} else { } else {
@ -1292,6 +1289,10 @@ class EHentai(
private const val BLANK_THUMB = "blank.gif" private const val BLANK_THUMB = "blank.gif"
private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB" private const val BLANK_PREVIEW_THUMB = "https://$THUMB_DOMAIN/g/$BLANK_THUMB"
private val MATCH_YEAR_REGEX = "^\\d{4}\$".toRegex()
private val MATCH_SEEK_REGEX = "^\\d{2,4}-\\d{1,2}".toRegex()
private val MATCH_JUMP_REGEX = "^\\d+(\$|d\$|w\$|m\$|y\$|-\$)".toRegex()
private const val EH_API_BASE = "https://api.e-hentai.org/api.php" private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!! private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!

View File

@ -38,15 +38,16 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
holder.itemView.context, holder.itemView.context,
android.R.layout.simple_dropdown_item_1line, android.R.layout.simple_dropdown_item_1line,
filter.values, filter.values,
filter.excludePrefix, filter.validPrefixes,
), ),
) )
holder.autoComplete.threshold = 3 holder.autoComplete.threshold = 3
// select from auto complete // select from auto complete
holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ -> holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ ->
val name = adapterView.getItemAtPosition(chipPosition) as String var name = (adapterView.getItemAtPosition(chipPosition) as String).trim()
if (name !in if (filter.excludePrefix != null && name.startsWith(filter.excludePrefix!!)) filter.skipAutoFillTags.map { filter.excludePrefix + it } else filter.skipAutoFillTags) { filter.validPrefixes.find { name.startsWith(it) }?.let { name = name.removePrefix(it).trim() }
if (name !in filter.skipAutoFillTags) {
holder.autoComplete.text = null holder.autoComplete.text = null
addTag(name, holder) addTag(name, holder)
} }
@ -54,12 +55,14 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
// done keyboard button is pressed // done keyboard button is pressed
holder.autoComplete.setOnEditorActionListener { textView, actionId, _ -> holder.autoComplete.setOnEditorActionListener { textView, actionId, _ ->
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) { if (actionId != EditorInfo.IME_ACTION_DONE) return@setOnEditorActionListener false
var name = textView.text.toString().trim()
filter.validPrefixes.find { name.startsWith(it) }?.let { name = name.removePrefix(it).trim() }
if (name !in filter.skipAutoFillTags) {
textView.text = null textView.text = null
addTag(textView.text.toString(), holder) addTag(name, holder)
return@setOnEditorActionListener true
} }
false true
} }
// space or comma is detected // space or comma is detected
@ -69,7 +72,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
} }
if (it.last() == ',') { if (it.last() == ',') {
val name = it.substring(0, it.length - 1) val name = it.toString().dropLast(1).trim()
addTag(name, holder) addTag(name, holder)
holder.autoComplete.text = null holder.autoComplete.text = null

View File

@ -5,11 +5,11 @@ import android.widget.ArrayAdapter
import android.widget.Filter import android.widget.Filter
import android.widget.Filterable import android.widget.Filterable
class AutoCompleteAdapter(context: Context, resource: Int, var objects: List<String>, val excludePrefix: String?) : class AutoCompleteAdapter(context: Context, resource: Int, var objects: List<String>, val validPrefixes: List<String>) :
ArrayAdapter<String>(context, resource, objects), ArrayAdapter<String>(context, resource, objects),
Filterable { Filterable {
private var mOriginalValues: List<String>? = objects private val mOriginalValues: List<String> = objects
private var mFilter: ListFilter? = null private var mFilter: ListFilter? = null
override fun getCount(): Int { override fun getCount(): Int {
@ -28,21 +28,27 @@ class AutoCompleteAdapter(context: Context, resource: Int, var objects: List<Str
} }
private inner class ListFilter : Filter() { private inner class ListFilter : Filter() {
override fun performFiltering(prefix: CharSequence?): FilterResults { override fun performFiltering(constraint: CharSequence?): FilterResults {
val results = FilterResults() val results = FilterResults()
if (mOriginalValues == null) {
mOriginalValues = objects
}
if (prefix == null || prefix.isEmpty()) { if (constraint.isNullOrBlank()) {
val list = mOriginalValues!! val list = mOriginalValues
results.values = list results.values = list
results.count = list.size results.count = list.size
} else { } else {
val prefixString = prefix.toString() val constraintString = constraint.toString()
val containsPrefix: Boolean = excludePrefix?.let { prefixString.startsWith(it) } ?: false val prefix = validPrefixes.find { constraintString.startsWith(it) }
val filterResults = mOriginalValues!!.filter { it.contains(if (excludePrefix != null) prefixString.removePrefix(excludePrefix) else prefixString, true) } val constraintStringNoPrefix = if (prefix != null) {
results.values = if (containsPrefix) filterResults.map { excludePrefix + it } else filterResults constraintString.removePrefix(prefix)
} else {
constraintString
}
val filterResults = mOriginalValues.filter { it.contains(constraintStringNoPrefix, true) }
results.values = if (prefix != null) {
filterResults.map { prefix + it }
} else {
filterResults
}
results.count = filterResults.size results.count = filterResults.size
} }
return results return results

View File

@ -31,7 +31,7 @@ sealed class Filter<T>(val name: String, var state: T) {
val hint: String, val hint: String,
val values: List<String>, val values: List<String>,
val skipAutoFillTags: List<String> = emptyList(), val skipAutoFillTags: List<String> = emptyList(),
val excludePrefix: String? = null, val validPrefixes: List<String> = emptyList(),
state: List<String>, state: List<String>,
) : Filter<List<String>>(name, state) ) : Filter<List<String>>(name, state)
// SY <-- // SY <--

View File

@ -25,8 +25,54 @@ open /* SY <-- */ class MangasPage(open val mangas: List<SManga>, open val hasNe
fun copy(mangas: List<SManga> = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage { fun copy(mangas: List<SManga> = this.mangas, hasNextPage: Boolean = this.hasNextPage): MangasPage {
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
override fun toString(): String {
return "MangasPage(mangas=$mangas, hasNextPage=$hasNextPage)"
}
} }
// SY --> // SY -->
data class MetadataMangasPage(override val mangas: List<SManga>, override val hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage) class MetadataMangasPage(
override val mangas: List<SManga>,
override val hasNextPage: Boolean,
val mangasMetadata: List<RaisedSearchMetadata>,
val nextKey: Long? = null
) : MangasPage(mangas, hasNextPage) {
fun copy(
mangas: List<SManga> = this.mangas,
hasNextPage: Boolean = this.hasNextPage,
mangasMetadata: List<RaisedSearchMetadata> = this.mangasMetadata,
nextKey: Long? = this.nextKey
): MangasPage {
return MetadataMangasPage(mangas, hasNextPage, mangasMetadata, nextKey)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as MetadataMangasPage
if (mangas != other.mangas) return false
if (hasNextPage != other.hasNextPage) return false
if (mangasMetadata != other.mangasMetadata) return false
if (nextKey != other.nextKey) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + mangas.hashCode()
result = 31 * result + hasNextPage.hashCode()
result = 31 * result + mangasMetadata.hashCode()
result = 31 * result + nextKey.hashCode()
return result
}
override fun toString(): String {
return "MetadataMangasPage(mangas=$mangas, hasNextPage=$hasNextPage, mangasMetadata=$mangasMetadata, nextKey=$nextKey)"
}
}
// SY <-- // SY <--