Support new E-H/Exh search engine
This commit is contained in:
parent
eedfa44ec8
commit
797a9e6b4e
@ -3,68 +3,43 @@ package eu.kanade.data.source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
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.util.lang.awaitSingle
|
||||
import exh.metadata.metadata.EHentaiSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
|
||||
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
|
||||
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
val lastMangaLink = lastMangaLink
|
||||
|
||||
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
|
||||
}
|
||||
return LoadResult.Page(
|
||||
data = mangasPage.mangas
|
||||
.mapIndexed { index, sManga -> sManga to metadata.getOrNull(index) },
|
||||
prevKey = null,
|
||||
nextKey = mangasPage.nextKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,16 @@ abstract class SourcePagingSource(
|
||||
} catch (e: Exception) {
|
||||
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 -->
|
||||
val metadata = if (mangasPage is MetadataMangasPage) {
|
||||
mangasPage.mangasMetadata
|
||||
@ -46,6 +56,7 @@ abstract class SourcePagingSource(
|
||||
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Long, /*SY --> */ Pair<SManga, RaisedSearchMetadata?>/*SY <-- */>): Long? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
|
@ -191,14 +191,7 @@ class EHentai(
|
||||
tags += parsedTags
|
||||
|
||||
if (infoElements != null) {
|
||||
genre = getGenre(
|
||||
infoElements.getOrNull(1),
|
||||
genreString = infoElements.getOrNull(1)
|
||||
?.text()
|
||||
?.nullIfBlank()
|
||||
?.lowercase()
|
||||
?.replace(" ", ""),
|
||||
)
|
||||
genre = getGenre(infoElements.getOrNull(1))
|
||||
|
||||
datePosted = getDateTag(infoElements.getOrNull(2))
|
||||
|
||||
@ -208,13 +201,7 @@ class EHentai(
|
||||
|
||||
length = getPageCount(infoElements.getOrNull(5))
|
||||
} else {
|
||||
val parsedGenre = body.selectFirst(".gl1c div")
|
||||
genre = getGenre(
|
||||
genreString = parsedGenre?.text()
|
||||
?.nullIfBlank()
|
||||
?.lowercase()
|
||||
?.replace(" ", ""),
|
||||
)
|
||||
genre = getGenre(body.selectFirst(".gl1c div"))
|
||||
|
||||
val info = body.selectFirst(".gl2c")!!
|
||||
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 isReversed = parsedLocation != null && parsedLocation.queryParameterNames.contains(REVERSE_PARAM)
|
||||
|
||||
// Add to page if required
|
||||
val hasNextPage = if (parsedLocation == null ||
|
||||
!parsedLocation.queryParameterNames.contains(REVERSE_PARAM)
|
||||
) {
|
||||
select("a[onclick=return false]").last()
|
||||
?.let {
|
||||
it.text() == ">"
|
||||
}
|
||||
?: select(".searchnav >div > a")
|
||||
.any { it.attr("href").contains("next") }
|
||||
val hasNextPage = if (isReversed) {
|
||||
select(".searchnav >div > a")
|
||||
.any { "prev" in it.attr("href") }
|
||||
} 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")
|
||||
?.nullIfBlank()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'")
|
||||
?.trim()
|
||||
?.substringAfterLast('/')
|
||||
?.removeSuffix("'") ?: genreString
|
||||
?.removeSuffix("'")
|
||||
?: element?.text()
|
||||
?.nullIfBlank()
|
||||
?.lowercase()
|
||||
?.replace(" ", "")
|
||||
?.trim()
|
||||
}
|
||||
|
||||
private fun getDateTag(element: Element?): Long? {
|
||||
@ -315,8 +317,13 @@ class EHentai(
|
||||
/**
|
||||
* Parse a list of galleries
|
||||
*/
|
||||
private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { mangaFromSource ->
|
||||
MetadataMangasPage(mangaFromSource.first.map { it.manga }, mangaFromSource.second, mangaFromSource.first.map { it.metadata })
|
||||
private fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { (parsedManga, nextPage) ->
|
||||
MetadataMangasPage(
|
||||
parsedManga.map { it.manga },
|
||||
nextPage != null,
|
||||
parsedManga.map { it.metadata },
|
||||
nextPage,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> = getChapterList(manga) {}
|
||||
@ -447,7 +454,7 @@ class EHentai(
|
||||
return client.newCall(chapterPageRequest(np)).asObservableSuccess()
|
||||
}
|
||||
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 {
|
||||
@ -477,16 +484,14 @@ class EHentai(
|
||||
// Support direct URL importing
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
searchMangaRequestObservable(page, query, filters).flatMap {
|
||||
client.newCall(it).asObservableSuccess()
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}.checkValid()
|
||||
super.fetchSearchManga(page, query, filters).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 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) {
|
||||
uri.appendQueryParameter("f_apply", "Apply+Filter")
|
||||
@ -494,6 +499,20 @@ class EHentai(
|
||||
filters.forEach {
|
||||
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 {
|
||||
uri.appendPath("toplist.php")
|
||||
uri.appendQueryParameter("tl", toplist.index.toString())
|
||||
@ -506,52 +525,31 @@ class EHentai(
|
||||
null
|
||||
}
|
||||
|
||||
val request = exGet(uri.toString(), regularPage)
|
||||
|
||||
// Reverse search results on filter
|
||||
if (toplist == ToplistOption.NONE && filters.any { it is ReverseFilter && it.state }) {
|
||||
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)
|
||||
}
|
||||
return exGet(
|
||||
url = uri.toString(),
|
||||
next = if (!isReverseFilterEnabled) regularPage else null,
|
||||
prev = if (isReverseFilterEnabled) regularPage else null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)
|
||||
|
||||
override fun popularMangaParse(response: Response) = genericMangaParse(response)
|
||||
override fun searchMangaParse(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(
|
||||
if (next != null) {
|
||||
if (exh) {
|
||||
if (next > 1) {
|
||||
addParam(url, "next", next.toString())
|
||||
} else {
|
||||
url
|
||||
}
|
||||
} else {
|
||||
addParam(url, "page", (next - 1).toString())
|
||||
}
|
||||
} else {
|
||||
url
|
||||
when {
|
||||
next != null && next > 1 -> addParam(url, "next", next.toString())
|
||||
prev != null && prev > 0 -> addParam(url, "prev", prev.toString())
|
||||
else -> url
|
||||
},
|
||||
if (additionalHeaders != null) {
|
||||
val headers = headers.newBuilder()
|
||||
@ -815,7 +813,7 @@ class EHentai(
|
||||
// Next page
|
||||
|
||||
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())
|
||||
}
|
||||
@ -886,8 +884,6 @@ class EHentai(
|
||||
|
||||
// Filters
|
||||
override fun getFilterList(): FilterList {
|
||||
val excludePrefix = "-"
|
||||
|
||||
return FilterList(
|
||||
*if (exh) {
|
||||
emptyArray()
|
||||
@ -898,19 +894,12 @@ class EHentai(
|
||||
Filter.Separator(),
|
||||
)
|
||||
},
|
||||
AutoCompleteTags(
|
||||
EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(),
|
||||
EHTags.getNamespaces().map { "$it:" },
|
||||
excludePrefix,
|
||||
),
|
||||
if (preferences.exhWatchedListDefaultState().get()) {
|
||||
Watched(isEnabled = true)
|
||||
} else {
|
||||
Watched(isEnabled = false)
|
||||
},
|
||||
AutoCompleteTags(),
|
||||
Watched(isEnabled = preferences.exhWatchedListDefaultState().get()),
|
||||
GenreGroup(),
|
||||
AdvancedGroup(),
|
||||
ReverseFilter(),
|
||||
JumpSeekFilter(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -992,9 +981,16 @@ class EHentai(
|
||||
filter.state.trimAll().dropBlank().mapNotNull { tag ->
|
||||
val split = tag.split(":").filterNot { it.isBlank() }
|
||||
if (split.size > 1) {
|
||||
val namespace = split[0].removePrefix("-")
|
||||
val namespace = split[0].removePrefix("-").removePrefix("~")
|
||||
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 {
|
||||
null
|
||||
}
|
||||
@ -1003,10 +999,12 @@ class EHentai(
|
||||
|
||||
advSearch.forEach { entry ->
|
||||
if (entry.exclude) stringBuilder.append("-")
|
||||
if (entry.or) stringBuilder.append("~")
|
||||
val namespace = entry.search.first?.let { "$it:" }.orEmpty()
|
||||
if (entry.search.second.contains(" ")) {
|
||||
stringBuilder.append(("""${entry.search.first}:"${entry.search.second}$""""))
|
||||
stringBuilder.append(("""$namespace"${entry.search.second}$""""))
|
||||
} else {
|
||||
stringBuilder.append("${entry.search.first}:${entry.search.second}$")
|
||||
stringBuilder.append("$namespace${entry.search.second}$")
|
||||
}
|
||||
stringBuilder.append(" ")
|
||||
}
|
||||
@ -1014,15 +1012,15 @@ class EHentai(
|
||||
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(
|
||||
name = "Tags",
|
||||
hint = "Search tags here (limit of 8)",
|
||||
values = tags,
|
||||
skipAutoFillTags = skipAutoFillTags,
|
||||
excludePrefix = excludePrefix,
|
||||
values = EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(),
|
||||
skipAutoFillTags = EHTags.getNamespaces().map { "$it:" },
|
||||
validPrefixes = listOf("-", "~"),
|
||||
state = emptyList(),
|
||||
)
|
||||
|
||||
@ -1052,22 +1050,21 @@ class EHentai(
|
||||
class AdvancedGroup : UriGroup<Filter<*>>(
|
||||
"Advanced Options",
|
||||
listOf(
|
||||
AdvancedOption("Search Gallery Name", "f_sname", true),
|
||||
AdvancedOption("Search Gallery Tags", "f_stags", true),
|
||||
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"),
|
||||
AdvancedOption("Browse Expunged Galleries", "f_sh"),
|
||||
AdvancedOption("Require Gallery Torrent", "f_sto"),
|
||||
RatingOption(),
|
||||
MinPagesOption(),
|
||||
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 JumpSeekFilter : Filter.Text("Jump/Seek")
|
||||
|
||||
override val name = if (exh) {
|
||||
"ExHentai"
|
||||
} else {
|
||||
@ -1292,6 +1289,10 @@ class EHentai(
|
||||
private const val BLANK_THUMB = "blank.gif"
|
||||
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 val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||
|
||||
|
@ -38,15 +38,16 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
||||
holder.itemView.context,
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
filter.values,
|
||||
filter.excludePrefix,
|
||||
filter.validPrefixes,
|
||||
),
|
||||
)
|
||||
holder.autoComplete.threshold = 3
|
||||
|
||||
// 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) {
|
||||
var name = (adapterView.getItemAtPosition(chipPosition) as String).trim()
|
||||
filter.validPrefixes.find { name.startsWith(it) }?.let { name = name.removePrefix(it).trim() }
|
||||
if (name !in filter.skipAutoFillTags) {
|
||||
holder.autoComplete.text = null
|
||||
addTag(name, holder)
|
||||
}
|
||||
@ -54,12 +55,14 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
||||
|
||||
// done keyboard button is pressed
|
||||
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
|
||||
addTag(textView.text.toString(), holder)
|
||||
return@setOnEditorActionListener true
|
||||
addTag(name, holder)
|
||||
}
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
// space or comma is detected
|
||||
@ -69,7 +72,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
|
||||
}
|
||||
|
||||
if (it.last() == ',') {
|
||||
val name = it.substring(0, it.length - 1)
|
||||
val name = it.toString().dropLast(1).trim()
|
||||
addTag(name, holder)
|
||||
|
||||
holder.autoComplete.text = null
|
||||
|
@ -5,11 +5,11 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
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),
|
||||
Filterable {
|
||||
|
||||
private var mOriginalValues: List<String>? = objects
|
||||
private val mOriginalValues: List<String> = objects
|
||||
private var mFilter: ListFilter? = null
|
||||
|
||||
override fun getCount(): Int {
|
||||
@ -28,21 +28,27 @@ class AutoCompleteAdapter(context: Context, resource: Int, var objects: List<Str
|
||||
}
|
||||
|
||||
private inner class ListFilter : Filter() {
|
||||
override fun performFiltering(prefix: CharSequence?): FilterResults {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val results = FilterResults()
|
||||
if (mOriginalValues == null) {
|
||||
mOriginalValues = objects
|
||||
}
|
||||
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
val list = mOriginalValues!!
|
||||
if (constraint.isNullOrBlank()) {
|
||||
val list = mOriginalValues
|
||||
results.values = list
|
||||
results.count = list.size
|
||||
} else {
|
||||
val prefixString = prefix.toString()
|
||||
val containsPrefix: Boolean = excludePrefix?.let { prefixString.startsWith(it) } ?: false
|
||||
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
|
||||
val constraintString = constraint.toString()
|
||||
val prefix = validPrefixes.find { constraintString.startsWith(it) }
|
||||
val constraintStringNoPrefix = if (prefix != null) {
|
||||
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
|
||||
}
|
||||
return results
|
||||
|
@ -31,7 +31,7 @@ sealed class Filter<T>(val name: String, var state: T) {
|
||||
val hint: String,
|
||||
val values: List<String>,
|
||||
val skipAutoFillTags: List<String> = emptyList(),
|
||||
val excludePrefix: String? = null,
|
||||
val validPrefixes: List<String> = emptyList(),
|
||||
state: List<String>,
|
||||
) : Filter<List<String>>(name, state)
|
||||
// SY <--
|
||||
|
@ -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 {
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "MangasPage(mangas=$mangas, hasNextPage=$hasNextPage)"
|
||||
}
|
||||
}
|
||||
|
||||
// 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 <--
|
||||
|
Loading…
x
Reference in New Issue
Block a user