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.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 ->
|
||||||
|
@ -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()!!
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 <--
|
||||||
|
@ -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 <--
|
||||||
|
Loading…
x
Reference in New Issue
Block a user