mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Add HBrowse
This commit is contained in:
		@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import exh.eh.EHentaiUpdateHelper
 | 
			
		||||
import io.noties.markwon.Markwon
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import uy.kohesive.injekt.api.*
 | 
			
		||||
@@ -44,6 +45,8 @@ class AppModule(val app: Application) : InjektModule {
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory { EHentaiUpdateHelper(app) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory { Markwon.create(app) }
 | 
			
		||||
 | 
			
		||||
        // Asynchronously init expensive components for a faster cold start
 | 
			
		||||
 | 
			
		||||
        rxAsync { get<PreferencesHelper>() }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.Pururin
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.*
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import exh.EH_SOURCE_ID
 | 
			
		||||
import exh.EXH_SOURCE_ID
 | 
			
		||||
@@ -116,6 +113,7 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
        exSrcs += Tsumino(context)
 | 
			
		||||
        exSrcs += Hitomi()
 | 
			
		||||
        exSrcs += EightMuses()
 | 
			
		||||
        exSrcs += HBrowse()
 | 
			
		||||
        return exSrcs
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,10 @@ package eu.kanade.tachiyomi.source.model
 | 
			
		||||
 | 
			
		||||
sealed class Filter<T>(val name: String, var state: T) {
 | 
			
		||||
    open class Header(name: String) : Filter<Any>(name, 0)
 | 
			
		||||
    // --> EXH
 | 
			
		||||
    // name = button text
 | 
			
		||||
    open class HelpDialog(name: String, val dialogTitle: String = name, val markdown: String) : Filter<Any>(name, 0)
 | 
			
		||||
    // <-- EXH
 | 
			
		||||
    open class Separator(name: String = "") : Filter<Any>(name, 0)
 | 
			
		||||
    abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
 | 
			
		||||
    abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,955 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online.english
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.github.salomonbrys.kotson.array
 | 
			
		||||
import com.github.salomonbrys.kotson.string
 | 
			
		||||
import com.google.gson.JsonParser
 | 
			
		||||
import com.lvla.rxjava.interopkt.toV1Single
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservable
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LewdSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import exh.metadata.metadata.HBrowseSearchMetadata
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.search.Namespace
 | 
			
		||||
import exh.search.SearchEngine
 | 
			
		||||
import exh.search.Text
 | 
			
		||||
import exh.util.await
 | 
			
		||||
import exh.util.dropBlank
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import info.debatty.java.stringsimilarity.Levenshtein
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.async
 | 
			
		||||
import kotlinx.coroutines.rx2.asSingle
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import kotlin.math.ceil
 | 
			
		||||
 | 
			
		||||
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
 | 
			
		||||
    /**
 | 
			
		||||
     * An ISO 639-1 compliant language code (two letters in lower case).
 | 
			
		||||
     */
 | 
			
		||||
    override val lang: String = "en"
 | 
			
		||||
    /**
 | 
			
		||||
     * Base url of the website without the trailing slash, like: http://mysite.com
 | 
			
		||||
     */
 | 
			
		||||
    override val baseUrl = HBrowseSearchMetadata.BASE_URL
 | 
			
		||||
 | 
			
		||||
    override val name: String = "HBrowse"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val metaClass = HBrowseSearchMetadata::class
 | 
			
		||||
 | 
			
		||||
    override fun headersBuilder() = Headers.Builder()
 | 
			
		||||
            .add("Cookie", BASE_COOKIES)
 | 
			
		||||
 | 
			
		||||
    private val clientWithoutCookies = client.newBuilder()
 | 
			
		||||
            .cookieJar(CookieJar.NO_COOKIES)
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    private val nonRedirectingClientWithoutCookies = clientWithoutCookies.newBuilder()
 | 
			
		||||
            .followRedirects(false)
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    private val searchEngine = SearchEngine()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the popular manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun popularMangaRequest(page: Int)
 | 
			
		||||
            = GET("$baseUrl/browse/title/rank/DESC", headers)
 | 
			
		||||
 | 
			
		||||
    private fun parseListing(response: Response): MangasPage {
 | 
			
		||||
        val doc = response.asJsoup()
 | 
			
		||||
        val main = doc.selectFirst("#main")
 | 
			
		||||
        val items = main.select(".thumbTable > tbody")
 | 
			
		||||
        val manga = items.map { mangaEle ->
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                val thumbElement = mangaEle.selectFirst(".thumbImg")
 | 
			
		||||
                url = "/" + thumbElement.parent().attr("href").split("/").dropBlank().first()
 | 
			
		||||
                title = thumbElement.parent().attr("title").substringAfter('\'').substringBeforeLast('\'')
 | 
			
		||||
                thumbnail_url = baseUrl + thumbElement.attr("src")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
 | 
			
		||||
        return MangasPage(
 | 
			
		||||
                manga,
 | 
			
		||||
                hasNextPage
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable containing a page with a list of manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        return urlImportFetchSearchManga(query) {
 | 
			
		||||
            fetchSearchMangaInternal(page, query, filters)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun popularMangaParse(response: Response) = parseListing(response)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the search manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
 | 
			
		||||
            = throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
 | 
			
		||||
    private fun fetchSearchMangaInternal(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        return GlobalScope.async(Dispatchers.IO) {
 | 
			
		||||
            val modeFilter = filters.filterIsInstance<ModeFilter>().firstOrNull()
 | 
			
		||||
            val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()
 | 
			
		||||
 | 
			
		||||
            var base: String? = null
 | 
			
		||||
            var isSortFilter = false
 | 
			
		||||
            // <NS, VALUE, EXCLUDED>
 | 
			
		||||
            var tagQuery: List<Triple<String, String, Boolean>>? = null
 | 
			
		||||
 | 
			
		||||
            if(sortFilter != null) {
 | 
			
		||||
                sortFilter.state?.let { state ->
 | 
			
		||||
                    if(query.isNotBlank()) {
 | 
			
		||||
                        throw IllegalArgumentException("Cannot use sorting while text/tag search is active!")
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    isSortFilter = true
 | 
			
		||||
                    base = "/browse/title/${SortFilter.SORT_OPTIONS[state.index].first}/${if(state.ascending) "ASC" else "DESC"}"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if(base == null) {
 | 
			
		||||
                base = if(modeFilter != null && modeFilter.state == 1) {
 | 
			
		||||
                    tagQuery = searchEngine.parseQuery(query, false).map {
 | 
			
		||||
                        when (it) {
 | 
			
		||||
                            is Text -> {
 | 
			
		||||
                                var minDist = Int.MAX_VALUE.toDouble()
 | 
			
		||||
                                // ns, value
 | 
			
		||||
                                var minContent: Pair<String, String> = "" to ""
 | 
			
		||||
                                for(ns in ALL_TAGS) {
 | 
			
		||||
                                    val (v, d) = ns.value.nearest(query, minDist)
 | 
			
		||||
                                    if(d < minDist) {
 | 
			
		||||
                                        minDist = d
 | 
			
		||||
                                        minContent = ns.key to v
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                minContent
 | 
			
		||||
                            }
 | 
			
		||||
                            is Namespace -> {
 | 
			
		||||
                                // Map ns aliases
 | 
			
		||||
                                val mappedNs = NS_MAPPINGS[it.namespace] ?: it.namespace
 | 
			
		||||
 | 
			
		||||
                                var key = mappedNs
 | 
			
		||||
                                if(ALL_TAGS.containsKey(key)) key = ALL_TAGS.keys.sorted().nearest(mappedNs).first
 | 
			
		||||
 | 
			
		||||
                                // Find nearest NS
 | 
			
		||||
                                val nsContents = ALL_TAGS[key]
 | 
			
		||||
 | 
			
		||||
                                key to nsContents!!.nearest(it.tag?.rawTextOnly() ?: "").first
 | 
			
		||||
                            }
 | 
			
		||||
                            else -> error("Unknown type!")
 | 
			
		||||
                        }.let { p ->
 | 
			
		||||
                            Triple(p.first, p.second, it.excluded)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    "/result"
 | 
			
		||||
                } else {
 | 
			
		||||
                    "/search"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            base += "/$page"
 | 
			
		||||
 | 
			
		||||
            if(isSortFilter) {
 | 
			
		||||
                parseListing(client.newCall(GET(baseUrl + base, headers))
 | 
			
		||||
                        .asObservableSuccess()
 | 
			
		||||
                        .toSingle()
 | 
			
		||||
                        .await(Schedulers.io()))
 | 
			
		||||
            } else {
 | 
			
		||||
                val body = if(tagQuery != null) {
 | 
			
		||||
                    FormBody.Builder()
 | 
			
		||||
                            .add("type", "advance")
 | 
			
		||||
                            .apply {
 | 
			
		||||
                                tagQuery.forEach {
 | 
			
		||||
                                    add(it.first + "_" + it.second, if(it.third) "n" else "y")
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                } else {
 | 
			
		||||
                    FormBody.Builder()
 | 
			
		||||
                            .add("type", "search")
 | 
			
		||||
                            .add("needle", query)
 | 
			
		||||
                }
 | 
			
		||||
                val processRequest = POST(
 | 
			
		||||
                        "$baseUrl/content/process.php",
 | 
			
		||||
                        headers,
 | 
			
		||||
                        body = body.build()
 | 
			
		||||
                )
 | 
			
		||||
                val processResponse = nonRedirectingClientWithoutCookies.newCall(processRequest)
 | 
			
		||||
                        .asObservable()
 | 
			
		||||
                        .toSingle()
 | 
			
		||||
                        .await(Schedulers.io())
 | 
			
		||||
 | 
			
		||||
                if(!processResponse.isRedirect)
 | 
			
		||||
                    throw IllegalStateException("Unexpected process response code!")
 | 
			
		||||
 | 
			
		||||
                val sessId = processResponse.headers("Set-Cookie").find {
 | 
			
		||||
                    it.startsWith("PHPSESSID")
 | 
			
		||||
                } ?: throw IllegalStateException("Missing server session cookie!")
 | 
			
		||||
 | 
			
		||||
                val response = clientWithoutCookies.newCall(GET(baseUrl + base,
 | 
			
		||||
                        headersBuilder()
 | 
			
		||||
                                .set("Cookie", BASE_COOKIES + " " + sessId.substringBefore(';'))
 | 
			
		||||
                                .build()))
 | 
			
		||||
                        .asObservableSuccess()
 | 
			
		||||
                        .toSingle()
 | 
			
		||||
                        .await(Schedulers.io())
 | 
			
		||||
 | 
			
		||||
                val doc = response.asJsoup()
 | 
			
		||||
                val manga = doc.select(".browseDescription").map {
 | 
			
		||||
                    SManga.create().apply {
 | 
			
		||||
                        val first = it.child(0)
 | 
			
		||||
                        url = first.attr("href")
 | 
			
		||||
                        title = first.attr("title").substringAfter('\'').removeSuffix("'").replace('_', ' ')
 | 
			
		||||
                        thumbnail_url = HBrowseSearchMetadata.guessThumbnailUrl(url.substring(1))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
 | 
			
		||||
                MangasPage(
 | 
			
		||||
                        manga,
 | 
			
		||||
                        hasNextPage
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }.asSingle(GlobalScope.coroutineContext).toV1Single().toObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Collection must be sorted and cannot be sorted
 | 
			
		||||
    private fun List<String>.nearest(string: String, maxDist: Double = Int.MAX_VALUE.toDouble()): Pair<String, Double> {
 | 
			
		||||
        val idx = binarySearch(string)
 | 
			
		||||
        return if(idx < 0) {
 | 
			
		||||
            val l = Levenshtein()
 | 
			
		||||
            var minSoFar = maxDist
 | 
			
		||||
            var minIndexSoFar = 0
 | 
			
		||||
            forEachIndexed { index, s ->
 | 
			
		||||
                val d = l.distance(string, s, ceil(minSoFar).toInt())
 | 
			
		||||
                if(d < minSoFar) {
 | 
			
		||||
                    minSoFar = d
 | 
			
		||||
                    minIndexSoFar = index
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            get(minIndexSoFar) to minSoFar
 | 
			
		||||
        } else {
 | 
			
		||||
            get(idx) to 0.0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaParse(response: Response) = parseListing(response)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for latest manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse/title/date/DESC", headers)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun latestUpdatesParse(response: Response) = parseListing(response)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the details of a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
 | 
			
		||||
        val tables = parseIntoTables(input)
 | 
			
		||||
        with(metadata) {
 | 
			
		||||
            hbId = Uri.parse(input.location()).pathSegments.first().toLong()
 | 
			
		||||
 | 
			
		||||
            tags.clear()
 | 
			
		||||
            (tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
 | 
			
		||||
                when(val lowercaseNs = k.toLowerCase()) {
 | 
			
		||||
                    "title" -> title = v.text()
 | 
			
		||||
                    "length" -> length = v.text().substringBefore(" ").toInt()
 | 
			
		||||
                    else -> {
 | 
			
		||||
                        v.getElementsByTag("a").forEach {
 | 
			
		||||
                            tags += RaisedTag(
 | 
			
		||||
                                    lowercaseNs,
 | 
			
		||||
                                    it.text(),
 | 
			
		||||
                                    HBrowseSearchMetadata.TAG_TYPE_DEFAULT
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated details for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
			
		||||
        return client.newCall(mangaDetailsRequest(manga))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .flatMap {
 | 
			
		||||
                    parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of chapters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        return parseIntoTables(response.asJsoup())["read manga online"]?.map { (key, value) ->
 | 
			
		||||
            SChapter.create().apply {
 | 
			
		||||
                url = value.selectFirst(".listLink").attr("href")
 | 
			
		||||
 | 
			
		||||
                name = key
 | 
			
		||||
            }
 | 
			
		||||
        } ?: emptyList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
 | 
			
		||||
        return doc.select("#main > .listTable").map { ele ->
 | 
			
		||||
            val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
 | 
			
		||||
            tableName to ele.select("tr").map {
 | 
			
		||||
                it.child(0).text() to it.child(1)
 | 
			
		||||
            }.toMap()
 | 
			
		||||
        }.toMap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of pages.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val doc = response.asJsoup()
 | 
			
		||||
        val basePath = listOf("data") + response.request().url().pathSegments()
 | 
			
		||||
        val scripts = doc.getElementsByTag("script").map { it.data() }
 | 
			
		||||
        for(script in scripts) {
 | 
			
		||||
            val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull() ?: continue
 | 
			
		||||
            val pageList = PAGE_LIST_REGEX.find(script)?.groupValues?.getOrNull(1) ?: continue
 | 
			
		||||
 | 
			
		||||
            return jsonParser.parse(pageList).array.take(totalPages).map {
 | 
			
		||||
                it.string
 | 
			
		||||
            }.mapIndexed { index, pageName ->
 | 
			
		||||
                Page(
 | 
			
		||||
                        index,
 | 
			
		||||
                        pageName,
 | 
			
		||||
                        "$baseUrl/${basePath.joinToString("/")}/$pageName"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return emptyList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class HelpFilter : Filter.HelpDialog("Usage instructions", markdown = """
 | 
			
		||||
        ### Modes
 | 
			
		||||
        There are three available filter modes:
 | 
			
		||||
        - Text search
 | 
			
		||||
        - Tag search
 | 
			
		||||
        - Sort mode
 | 
			
		||||
        
 | 
			
		||||
        You can only use a single mode at a time. Switch between the text and tag search modes using the dropdown menu. Switch to sorting mode by selecting a sorting option.
 | 
			
		||||
        
 | 
			
		||||
        ### Text search
 | 
			
		||||
        Search for galleries by title, artist or origin.
 | 
			
		||||
        
 | 
			
		||||
        ### Tag search
 | 
			
		||||
        Search for galleries by tag (e.g. search for a specific genre, type, setting, etc). Uses nhentai/e-hentai syntax. Refer to the "Search" section on [this page](https://nhentai.net/info/) for more information.
 | 
			
		||||
        
 | 
			
		||||
        ### Sort mode
 | 
			
		||||
        View a list of all galleries sorted by a specific parameter. Exit sorting mode by resetting the filters using the reset button near the bottom of the screen.
 | 
			
		||||
        
 | 
			
		||||
        ### Tag list
 | 
			
		||||
    """.trimIndent() + "\n$TAGS_AS_MARKDOWN")
 | 
			
		||||
 | 
			
		||||
    class ModeFilter : Filter.Select<String>("Mode", arrayOf(
 | 
			
		||||
            "Text search",
 | 
			
		||||
            "Tag search"
 | 
			
		||||
    ))
 | 
			
		||||
 | 
			
		||||
    class SortFilter : Filter.Sort("Sort", SORT_OPTIONS.map { it.second }.toTypedArray()) {
 | 
			
		||||
        companion object {
 | 
			
		||||
            // internal to display
 | 
			
		||||
            val SORT_OPTIONS = listOf(
 | 
			
		||||
                    "length" to "Length",
 | 
			
		||||
                    "date" to "Date added",
 | 
			
		||||
                    "rank" to "Rank"
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList() = FilterList(
 | 
			
		||||
            HelpFilter(),
 | 
			
		||||
            ModeFilter(),
 | 
			
		||||
            SortFilter()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the absolute url to the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun imageUrlParse(response: Response): String {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val matchingHosts = listOf(
 | 
			
		||||
            "www.hbrowse.com",
 | 
			
		||||
            "hbrowse.com"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun mapUrlToMangaUrl(uri: Uri): String? {
 | 
			
		||||
        return "$baseUrl/${uri.pathSegments.first()}"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);")
 | 
			
		||||
        private val TOTAL_PAGES_REGEX = Regex("totalPages *= *([0-9]*);")
 | 
			
		||||
 | 
			
		||||
        private val jsonParser by lazy { JsonParser() }
 | 
			
		||||
 | 
			
		||||
        private const val BASE_COOKIES = "thumbnails=1;"
 | 
			
		||||
 | 
			
		||||
        private val NS_MAPPINGS = mapOf(
 | 
			
		||||
                "set" to "setting",
 | 
			
		||||
                "loc" to "setting",
 | 
			
		||||
                "location" to "setting",
 | 
			
		||||
                "fet" to "fetish",
 | 
			
		||||
                "relation" to "relationship",
 | 
			
		||||
                "male" to "malebody",
 | 
			
		||||
                "female" to "femalebody",
 | 
			
		||||
                "pos" to "position"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        private val ALL_TAGS = mapOf(
 | 
			
		||||
                "genre" to listOf(
 | 
			
		||||
                        "action",
 | 
			
		||||
                        "adventure",
 | 
			
		||||
                        "anime",
 | 
			
		||||
                        "bizarre",
 | 
			
		||||
                        "comedy",
 | 
			
		||||
                        "drama",
 | 
			
		||||
                        "fantasy",
 | 
			
		||||
                        "gore",
 | 
			
		||||
                        "historic",
 | 
			
		||||
                        "horror",
 | 
			
		||||
                        "medieval",
 | 
			
		||||
                        "modern",
 | 
			
		||||
                        "myth",
 | 
			
		||||
                        "psychological",
 | 
			
		||||
                        "romance",
 | 
			
		||||
                        "school_life",
 | 
			
		||||
                        "scifi",
 | 
			
		||||
                        "supernatural",
 | 
			
		||||
                        "video_game",
 | 
			
		||||
                        "visual_novel"
 | 
			
		||||
                ),
 | 
			
		||||
                "type" to listOf(
 | 
			
		||||
                        "anthology",
 | 
			
		||||
                        "bestiality",
 | 
			
		||||
                        "dandere",
 | 
			
		||||
                        "deredere",
 | 
			
		||||
                        "deviant",
 | 
			
		||||
                        "fully_colored",
 | 
			
		||||
                        "furry",
 | 
			
		||||
                        "futanari",
 | 
			
		||||
                        "gender_bender",
 | 
			
		||||
                        "guro",
 | 
			
		||||
                        "harem",
 | 
			
		||||
                        "incest",
 | 
			
		||||
                        "kuudere",
 | 
			
		||||
                        "lolicon",
 | 
			
		||||
                        "long_story",
 | 
			
		||||
                        "netorare",
 | 
			
		||||
                        "non-con",
 | 
			
		||||
                        "partly_colored",
 | 
			
		||||
                        "reverse_harem",
 | 
			
		||||
                        "ryona",
 | 
			
		||||
                        "short_story",
 | 
			
		||||
                        "shotacon",
 | 
			
		||||
                        "transgender",
 | 
			
		||||
                        "tsundere",
 | 
			
		||||
                        "uncensored",
 | 
			
		||||
                        "vanilla",
 | 
			
		||||
                        "yandere",
 | 
			
		||||
                        "yaoi",
 | 
			
		||||
                        "yuri"
 | 
			
		||||
                ),
 | 
			
		||||
                "setting" to listOf(
 | 
			
		||||
                        "amusement_park",
 | 
			
		||||
                        "attic",
 | 
			
		||||
                        "automobile",
 | 
			
		||||
                        "balcony",
 | 
			
		||||
                        "basement",
 | 
			
		||||
                        "bath",
 | 
			
		||||
                        "beach",
 | 
			
		||||
                        "bedroom",
 | 
			
		||||
                        "cabin",
 | 
			
		||||
                        "castle",
 | 
			
		||||
                        "cave",
 | 
			
		||||
                        "church",
 | 
			
		||||
                        "classroom",
 | 
			
		||||
                        "deck",
 | 
			
		||||
                        "dining_room",
 | 
			
		||||
                        "doctors",
 | 
			
		||||
                        "dojo",
 | 
			
		||||
                        "doorway",
 | 
			
		||||
                        "dream",
 | 
			
		||||
                        "dressing_room",
 | 
			
		||||
                        "dungeon",
 | 
			
		||||
                        "elevator",
 | 
			
		||||
                        "festival",
 | 
			
		||||
                        "gym",
 | 
			
		||||
                        "haunted_building",
 | 
			
		||||
                        "hospital",
 | 
			
		||||
                        "hotel",
 | 
			
		||||
                        "hot_springs",
 | 
			
		||||
                        "kitchen",
 | 
			
		||||
                        "laboratory",
 | 
			
		||||
                        "library",
 | 
			
		||||
                        "living_room",
 | 
			
		||||
                        "locker_room",
 | 
			
		||||
                        "mansion",
 | 
			
		||||
                        "office",
 | 
			
		||||
                        "other",
 | 
			
		||||
                        "outdoor",
 | 
			
		||||
                        "outer_space",
 | 
			
		||||
                        "park",
 | 
			
		||||
                        "pool",
 | 
			
		||||
                        "prison",
 | 
			
		||||
                        "public",
 | 
			
		||||
                        "restaurant",
 | 
			
		||||
                        "restroom",
 | 
			
		||||
                        "roof",
 | 
			
		||||
                        "sauna",
 | 
			
		||||
                        "school",
 | 
			
		||||
                        "school_nurses_office",
 | 
			
		||||
                        "shower",
 | 
			
		||||
                        "shrine",
 | 
			
		||||
                        "storage_room",
 | 
			
		||||
                        "store",
 | 
			
		||||
                        "street",
 | 
			
		||||
                        "teachers_lounge",
 | 
			
		||||
                        "theater",
 | 
			
		||||
                        "tight_space",
 | 
			
		||||
                        "toilet",
 | 
			
		||||
                        "train",
 | 
			
		||||
                        "transit",
 | 
			
		||||
                        "virtual_reality",
 | 
			
		||||
                        "warehouse",
 | 
			
		||||
                        "wilderness"
 | 
			
		||||
                ),
 | 
			
		||||
                "fetish" to listOf(
 | 
			
		||||
                        "androphobia",
 | 
			
		||||
                        "apron",
 | 
			
		||||
                        "assertive_girl",
 | 
			
		||||
                        "bikini",
 | 
			
		||||
                        "bloomers",
 | 
			
		||||
                        "breast_expansion",
 | 
			
		||||
                        "business_suit",
 | 
			
		||||
                        "chastity_device",
 | 
			
		||||
                        "chinese_dress",
 | 
			
		||||
                        "christmas",
 | 
			
		||||
                        "collar",
 | 
			
		||||
                        "corset",
 | 
			
		||||
                        "cosplay_(female)",
 | 
			
		||||
                        "cosplay_(male)",
 | 
			
		||||
                        "crossdressing_(female)",
 | 
			
		||||
                        "crossdressing_(male)",
 | 
			
		||||
                        "eye_patch",
 | 
			
		||||
                        "food",
 | 
			
		||||
                        "giantess",
 | 
			
		||||
                        "glasses",
 | 
			
		||||
                        "gothic_lolita",
 | 
			
		||||
                        "gyaru",
 | 
			
		||||
                        "gynophobia",
 | 
			
		||||
                        "high_heels",
 | 
			
		||||
                        "hot_pants",
 | 
			
		||||
                        "impregnation",
 | 
			
		||||
                        "kemonomimi",
 | 
			
		||||
                        "kimono",
 | 
			
		||||
                        "knee_high_socks",
 | 
			
		||||
                        "lab_coat",
 | 
			
		||||
                        "latex",
 | 
			
		||||
                        "leotard",
 | 
			
		||||
                        "lingerie",
 | 
			
		||||
                        "maid_outfit",
 | 
			
		||||
                        "mother_and_daughter",
 | 
			
		||||
                        "none",
 | 
			
		||||
                        "nonhuman_girl",
 | 
			
		||||
                        "olfactophilia",
 | 
			
		||||
                        "pregnant",
 | 
			
		||||
                        "rich_girl",
 | 
			
		||||
                        "school_swimsuit",
 | 
			
		||||
                        "shy_girl",
 | 
			
		||||
                        "sisters",
 | 
			
		||||
                        "sleeping_girl",
 | 
			
		||||
                        "sporty",
 | 
			
		||||
                        "stockings",
 | 
			
		||||
                        "strapon",
 | 
			
		||||
                        "student_uniform",
 | 
			
		||||
                        "swimsuit",
 | 
			
		||||
                        "tanned",
 | 
			
		||||
                        "tattoo",
 | 
			
		||||
                        "time_stop",
 | 
			
		||||
                        "twins_(coed)",
 | 
			
		||||
                        "twins_(female)",
 | 
			
		||||
                        "twins_(male)",
 | 
			
		||||
                        "uniform",
 | 
			
		||||
                        "wedding_dress"
 | 
			
		||||
                ),
 | 
			
		||||
                "role" to listOf(
 | 
			
		||||
                        "alien",
 | 
			
		||||
                        "android",
 | 
			
		||||
                        "angel",
 | 
			
		||||
                        "athlete",
 | 
			
		||||
                        "bride",
 | 
			
		||||
                        "bunnygirl",
 | 
			
		||||
                        "cheerleader",
 | 
			
		||||
                        "delinquent",
 | 
			
		||||
                        "demon",
 | 
			
		||||
                        "doctor",
 | 
			
		||||
                        "dominatrix",
 | 
			
		||||
                        "escort",
 | 
			
		||||
                        "foreigner",
 | 
			
		||||
                        "ghost",
 | 
			
		||||
                        "housewife",
 | 
			
		||||
                        "idol",
 | 
			
		||||
                        "magical_girl",
 | 
			
		||||
                        "maid",
 | 
			
		||||
                        "mamono",
 | 
			
		||||
                        "massagist",
 | 
			
		||||
                        "miko",
 | 
			
		||||
                        "mythical_being",
 | 
			
		||||
                        "neet",
 | 
			
		||||
                        "nekomimi",
 | 
			
		||||
                        "newlywed",
 | 
			
		||||
                        "ninja",
 | 
			
		||||
                        "normal",
 | 
			
		||||
                        "nun",
 | 
			
		||||
                        "nurse",
 | 
			
		||||
                        "office_lady",
 | 
			
		||||
                        "other",
 | 
			
		||||
                        "police",
 | 
			
		||||
                        "priest",
 | 
			
		||||
                        "princess",
 | 
			
		||||
                        "queen",
 | 
			
		||||
                        "school_nurse",
 | 
			
		||||
                        "scientist",
 | 
			
		||||
                        "sorcerer",
 | 
			
		||||
                        "student",
 | 
			
		||||
                        "succubus",
 | 
			
		||||
                        "teacher",
 | 
			
		||||
                        "tomboy",
 | 
			
		||||
                        "tutor",
 | 
			
		||||
                        "waitress",
 | 
			
		||||
                        "warrior",
 | 
			
		||||
                        "witch"
 | 
			
		||||
                ),
 | 
			
		||||
                "relationship" to listOf(
 | 
			
		||||
                        "acquaintance",
 | 
			
		||||
                        "anothers_daughter",
 | 
			
		||||
                        "anothers_girlfriend",
 | 
			
		||||
                        "anothers_mother",
 | 
			
		||||
                        "anothers_sister",
 | 
			
		||||
                        "anothers_wife",
 | 
			
		||||
                        "aunt",
 | 
			
		||||
                        "babysitter",
 | 
			
		||||
                        "childhood_friend",
 | 
			
		||||
                        "classmate",
 | 
			
		||||
                        "cousin",
 | 
			
		||||
                        "customer",
 | 
			
		||||
                        "daughter",
 | 
			
		||||
                        "daughter-in-law",
 | 
			
		||||
                        "employee",
 | 
			
		||||
                        "employer",
 | 
			
		||||
                        "enemy",
 | 
			
		||||
                        "fiance",
 | 
			
		||||
                        "friend",
 | 
			
		||||
                        "friends_daughter",
 | 
			
		||||
                        "friends_girlfriend",
 | 
			
		||||
                        "friends_mother",
 | 
			
		||||
                        "friends_sister",
 | 
			
		||||
                        "friends_wife",
 | 
			
		||||
                        "girlfriend",
 | 
			
		||||
                        "landlord",
 | 
			
		||||
                        "manager",
 | 
			
		||||
                        "master",
 | 
			
		||||
                        "mother",
 | 
			
		||||
                        "mother-in-law",
 | 
			
		||||
                        "neighbor",
 | 
			
		||||
                        "niece",
 | 
			
		||||
                        "none",
 | 
			
		||||
                        "older_sister",
 | 
			
		||||
                        "patient",
 | 
			
		||||
                        "pet",
 | 
			
		||||
                        "physician",
 | 
			
		||||
                        "relative",
 | 
			
		||||
                        "relatives_friend",
 | 
			
		||||
                        "relatives_girlfriend",
 | 
			
		||||
                        "relatives_wife",
 | 
			
		||||
                        "servant",
 | 
			
		||||
                        "server",
 | 
			
		||||
                        "sister-in-law",
 | 
			
		||||
                        "slave",
 | 
			
		||||
                        "stepdaughter",
 | 
			
		||||
                        "stepmother",
 | 
			
		||||
                        "stepsister",
 | 
			
		||||
                        "stranger",
 | 
			
		||||
                        "student",
 | 
			
		||||
                        "teacher",
 | 
			
		||||
                        "tutee",
 | 
			
		||||
                        "tutor",
 | 
			
		||||
                        "twin",
 | 
			
		||||
                        "underclassman",
 | 
			
		||||
                        "upperclassman",
 | 
			
		||||
                        "wife",
 | 
			
		||||
                        "workmate",
 | 
			
		||||
                        "younger_sister"
 | 
			
		||||
                ),
 | 
			
		||||
                "malebody" to listOf(
 | 
			
		||||
                        "adult",
 | 
			
		||||
                        "animal",
 | 
			
		||||
                        "animal_ears",
 | 
			
		||||
                        "bald",
 | 
			
		||||
                        "beard",
 | 
			
		||||
                        "dark_skin",
 | 
			
		||||
                        "elderly",
 | 
			
		||||
                        "exaggerated_penis",
 | 
			
		||||
                        "fat",
 | 
			
		||||
                        "furry",
 | 
			
		||||
                        "goatee",
 | 
			
		||||
                        "hairy",
 | 
			
		||||
                        "half_animal",
 | 
			
		||||
                        "horns",
 | 
			
		||||
                        "large_penis",
 | 
			
		||||
                        "long_hair",
 | 
			
		||||
                        "middle_age",
 | 
			
		||||
                        "monster",
 | 
			
		||||
                        "muscular",
 | 
			
		||||
                        "mustache",
 | 
			
		||||
                        "none",
 | 
			
		||||
                        "short",
 | 
			
		||||
                        "short_hair",
 | 
			
		||||
                        "skinny",
 | 
			
		||||
                        "small_penis",
 | 
			
		||||
                        "tail",
 | 
			
		||||
                        "tall",
 | 
			
		||||
                        "tanned",
 | 
			
		||||
                        "tan_line",
 | 
			
		||||
                        "teenager",
 | 
			
		||||
                        "wings",
 | 
			
		||||
                        "young"
 | 
			
		||||
                ),
 | 
			
		||||
                "femalebody" to listOf(
 | 
			
		||||
                        "adult",
 | 
			
		||||
                        "animal_ears",
 | 
			
		||||
                        "bald",
 | 
			
		||||
                        "big_butt",
 | 
			
		||||
                        "chubby",
 | 
			
		||||
                        "dark_skin",
 | 
			
		||||
                        "elderly",
 | 
			
		||||
                        "elf_ears",
 | 
			
		||||
                        "exaggerated_breasts",
 | 
			
		||||
                        "fat",
 | 
			
		||||
                        "furry",
 | 
			
		||||
                        "hairy",
 | 
			
		||||
                        "hair_bun",
 | 
			
		||||
                        "half_animal",
 | 
			
		||||
                        "halo",
 | 
			
		||||
                        "hime_cut",
 | 
			
		||||
                        "horns",
 | 
			
		||||
                        "large_breasts",
 | 
			
		||||
                        "long_hair",
 | 
			
		||||
                        "middle_age",
 | 
			
		||||
                        "monster_girl",
 | 
			
		||||
                        "muscular",
 | 
			
		||||
                        "none",
 | 
			
		||||
                        "pigtails",
 | 
			
		||||
                        "ponytail",
 | 
			
		||||
                        "short",
 | 
			
		||||
                        "short_hair",
 | 
			
		||||
                        "skinny",
 | 
			
		||||
                        "small_breasts",
 | 
			
		||||
                        "tail",
 | 
			
		||||
                        "tall",
 | 
			
		||||
                        "tanned",
 | 
			
		||||
                        "tan_line",
 | 
			
		||||
                        "teenager",
 | 
			
		||||
                        "twintails",
 | 
			
		||||
                        "wings",
 | 
			
		||||
                        "young"
 | 
			
		||||
                ),
 | 
			
		||||
                "grouping" to listOf(
 | 
			
		||||
                        "foursome_(1_female)",
 | 
			
		||||
                        "foursome_(1_male)",
 | 
			
		||||
                        "foursome_(mixed)",
 | 
			
		||||
                        "foursome_(only_female)",
 | 
			
		||||
                        "one_on_one",
 | 
			
		||||
                        "one_on_one_(2_females)",
 | 
			
		||||
                        "one_on_one_(2_males)",
 | 
			
		||||
                        "orgy_(1_female)",
 | 
			
		||||
                        "orgy_(1_male)",
 | 
			
		||||
                        "orgy_(mainly_female)",
 | 
			
		||||
                        "orgy_(mainly_male)",
 | 
			
		||||
                        "orgy_(mixed)",
 | 
			
		||||
                        "orgy_(only_female)",
 | 
			
		||||
                        "orgy_(only_male)",
 | 
			
		||||
                        "solo_(female)",
 | 
			
		||||
                        "solo_(male)",
 | 
			
		||||
                        "threesome_(1_female)",
 | 
			
		||||
                        "threesome_(1_male)",
 | 
			
		||||
                        "threesome_(only_female)",
 | 
			
		||||
                        "threesome_(only_male)"
 | 
			
		||||
                ),
 | 
			
		||||
                "scene" to listOf(
 | 
			
		||||
                        "adultery",
 | 
			
		||||
                        "ahegao",
 | 
			
		||||
                        "anal_(female)",
 | 
			
		||||
                        "anal_(male)",
 | 
			
		||||
                        "aphrodisiac",
 | 
			
		||||
                        "armpit_sex",
 | 
			
		||||
                        "asphyxiation",
 | 
			
		||||
                        "blackmail",
 | 
			
		||||
                        "blowjob",
 | 
			
		||||
                        "bondage",
 | 
			
		||||
                        "breast_feeding",
 | 
			
		||||
                        "breast_sucking",
 | 
			
		||||
                        "bukkake",
 | 
			
		||||
                        "cheating_(female)",
 | 
			
		||||
                        "cheating_(male)",
 | 
			
		||||
                        "chikan",
 | 
			
		||||
                        "clothed_sex",
 | 
			
		||||
                        "consensual",
 | 
			
		||||
                        "cunnilingus",
 | 
			
		||||
                        "defloration",
 | 
			
		||||
                        "discipline",
 | 
			
		||||
                        "dominance",
 | 
			
		||||
                        "double_penetration",
 | 
			
		||||
                        "drunk",
 | 
			
		||||
                        "enema",
 | 
			
		||||
                        "exhibitionism",
 | 
			
		||||
                        "facesitting",
 | 
			
		||||
                        "fingering_(female)",
 | 
			
		||||
                        "fingering_(male)",
 | 
			
		||||
                        "fisting",
 | 
			
		||||
                        "footjob",
 | 
			
		||||
                        "grinding",
 | 
			
		||||
                        "groping",
 | 
			
		||||
                        "handjob",
 | 
			
		||||
                        "humiliation",
 | 
			
		||||
                        "hypnosis",
 | 
			
		||||
                        "intercrural",
 | 
			
		||||
                        "interracial_sex",
 | 
			
		||||
                        "interspecies_sex",
 | 
			
		||||
                        "lactation",
 | 
			
		||||
                        "lotion",
 | 
			
		||||
                        "masochism",
 | 
			
		||||
                        "masturbation",
 | 
			
		||||
                        "mind_break",
 | 
			
		||||
                        "nonhuman",
 | 
			
		||||
                        "orgy",
 | 
			
		||||
                        "paizuri",
 | 
			
		||||
                        "phone_sex",
 | 
			
		||||
                        "props",
 | 
			
		||||
                        "rape",
 | 
			
		||||
                        "reverse_rape",
 | 
			
		||||
                        "rimjob",
 | 
			
		||||
                        "sadism",
 | 
			
		||||
                        "scat",
 | 
			
		||||
                        "sex_toys",
 | 
			
		||||
                        "spanking",
 | 
			
		||||
                        "squirt",
 | 
			
		||||
                        "submission",
 | 
			
		||||
                        "sumata",
 | 
			
		||||
                        "swingers",
 | 
			
		||||
                        "tentacles",
 | 
			
		||||
                        "voyeurism",
 | 
			
		||||
                        "watersports",
 | 
			
		||||
                        "x-ray_blowjob",
 | 
			
		||||
                        "x-ray_sex"
 | 
			
		||||
                ),
 | 
			
		||||
                "position" to listOf(
 | 
			
		||||
                        "69",
 | 
			
		||||
                        "acrobat",
 | 
			
		||||
                        "arch",
 | 
			
		||||
                        "bodyguard",
 | 
			
		||||
                        "butterfly",
 | 
			
		||||
                        "cowgirl",
 | 
			
		||||
                        "dancer",
 | 
			
		||||
                        "deck_chair",
 | 
			
		||||
                        "deep_stick",
 | 
			
		||||
                        "doggy",
 | 
			
		||||
                        "drill",
 | 
			
		||||
                        "ex_sex",
 | 
			
		||||
                        "jockey",
 | 
			
		||||
                        "lap_dance",
 | 
			
		||||
                        "leg_glider",
 | 
			
		||||
                        "lotus",
 | 
			
		||||
                        "mastery",
 | 
			
		||||
                        "missionary",
 | 
			
		||||
                        "none",
 | 
			
		||||
                        "other",
 | 
			
		||||
                        "pile_driver",
 | 
			
		||||
                        "prison_guard",
 | 
			
		||||
                        "reverse_piggyback",
 | 
			
		||||
                        "rodeo",
 | 
			
		||||
                        "spoons",
 | 
			
		||||
                        "standing",
 | 
			
		||||
                        "teaspoons",
 | 
			
		||||
                        "unusual",
 | 
			
		||||
                        "victory"
 | 
			
		||||
                )
 | 
			
		||||
        ).mapValues { it.value.sorted() }
 | 
			
		||||
 | 
			
		||||
        private val TAGS_AS_MARKDOWN = ALL_TAGS.map { (ns, values) ->
 | 
			
		||||
            "#### $ns\n" + values.map { "- $it" }.joinToString("\n") }.joinToString("\n\n")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -292,6 +292,9 @@ open class BrowseCataloguePresenter(
 | 
			
		||||
        return mapNotNull {
 | 
			
		||||
            when (it) {
 | 
			
		||||
                is Filter.Header -> HeaderItem(it)
 | 
			
		||||
                // --> EXH
 | 
			
		||||
                is Filter.HelpDialog -> HelpDialogItem(it)
 | 
			
		||||
                // <-- EXH
 | 
			
		||||
                is Filter.Separator -> SeparatorItem(it)
 | 
			
		||||
                is Filter.CheckBox -> CheckboxItem(it)
 | 
			
		||||
                is Filter.TriState -> TriStateItem(it)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HelpDialogItem.kt
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										61
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/filter/HelpDialogItem.kt
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.filter
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import io.noties.markwon.Markwon
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
class HelpDialogItem(val filter: Filter.HelpDialog) : AbstractHeaderItem<HelpDialogItem.Holder>() {
 | 
			
		||||
    private val markwon: Markwon by injectLazy()
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("PrivateResource")
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.navigation_view_help_dialog
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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?>?) {
 | 
			
		||||
        val view = holder.button as TextView
 | 
			
		||||
        view.text = filter.name
 | 
			
		||||
        view.setOnClickListener {
 | 
			
		||||
            val v = TextView(view.context)
 | 
			
		||||
 | 
			
		||||
            val parsed = markwon.parse(filter.markdown)
 | 
			
		||||
            val rendered = markwon.render(parsed)
 | 
			
		||||
            markwon.setParsedMarkdown(v, rendered)
 | 
			
		||||
 | 
			
		||||
            MaterialDialog.Builder(view.context)
 | 
			
		||||
                    .title(filter.dialogTitle)
 | 
			
		||||
                    .customView(v, true)
 | 
			
		||||
                    .positiveText("Ok")
 | 
			
		||||
                    .show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
        return filter == (other as HelpDialogItem).filter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return filter.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
 | 
			
		||||
        val button: Button = itemView.findViewById(R.id.dialog_open_button)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ val PURURIN_SOURCE_ID = delegatedSourceId<Pururin>()
 | 
			
		||||
const val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9
 | 
			
		||||
const val HITOMI_SOURCE_ID = LEWD_SOURCE_SERIES + 10
 | 
			
		||||
const val EIGHTMUSES_SOURCE_ID = LEWD_SOURCE_SERIES + 11
 | 
			
		||||
const val HBROWSE_SOURCE_ID = LEWD_SOURCE_SERIES + 12
 | 
			
		||||
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
 | 
			
		||||
 | 
			
		||||
private val DELEGATED_LEWD_SOURCES = listOf(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package exh.metadata.metadata
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import exh.metadata.metadata.EightMusesSearchMetadata.Companion.ARTIST_NAMESPACE
 | 
			
		||||
import exh.metadata.metadata.base.RaisedSearchMetadata
 | 
			
		||||
import exh.plusAssign
 | 
			
		||||
 | 
			
		||||
class HBrowseSearchMetadata : RaisedSearchMetadata() {
 | 
			
		||||
    var hbId: Long? = null
 | 
			
		||||
 | 
			
		||||
    var title: String? by titleDelegate(TITLE_TYPE_MAIN)
 | 
			
		||||
 | 
			
		||||
    // Length in pages
 | 
			
		||||
    var length: Int? = null
 | 
			
		||||
 | 
			
		||||
    override fun copyTo(manga: SManga) {
 | 
			
		||||
        manga.url = "/$hbId"
 | 
			
		||||
 | 
			
		||||
        title?.let {
 | 
			
		||||
            manga.title = it
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Guess thumbnail URL if manga does not have thumbnail URL
 | 
			
		||||
        if(manga.thumbnail_url.isNullOrBlank()) {
 | 
			
		||||
            manga.thumbnail_url = guessThumbnailUrl(hbId.toString())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        manga.artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name }
 | 
			
		||||
 | 
			
		||||
        val titleDesc = StringBuilder()
 | 
			
		||||
        title?.let { titleDesc += "Title: $it\n" }
 | 
			
		||||
        length?.let { titleDesc += "Length: $it page(s)\n" }
 | 
			
		||||
 | 
			
		||||
        val tagsDesc = tagsToDescription()
 | 
			
		||||
 | 
			
		||||
        manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
 | 
			
		||||
                .filter(String::isNotBlank)
 | 
			
		||||
                .joinToString(separator = "\n")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val BASE_URL = "https://www.hbrowse.com"
 | 
			
		||||
 | 
			
		||||
        private const val TITLE_TYPE_MAIN = 0
 | 
			
		||||
 | 
			
		||||
        const val TAG_TYPE_DEFAULT = 0
 | 
			
		||||
 | 
			
		||||
        fun guessThumbnailUrl(hbid: String): String {
 | 
			
		||||
            return "$BASE_URL/thumbnails/${hbid}_1.jpg#guessed"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,10 @@
 | 
			
		||||
package exh.search
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
 | 
			
		||||
import exh.metadata.sql.tables.SearchMetadataTable
 | 
			
		||||
import exh.metadata.sql.tables.SearchTagTable
 | 
			
		||||
import exh.metadata.sql.tables.SearchTitleTable
 | 
			
		||||
 | 
			
		||||
class SearchEngine {
 | 
			
		||||
 | 
			
		||||
    private val queryCache = mutableMapOf<String, List<QueryComponent>>()
 | 
			
		||||
 | 
			
		||||
    fun textToSubQueries(namespace: String?,
 | 
			
		||||
@@ -116,7 +114,7 @@ class SearchEngine {
 | 
			
		||||
        return baseQuery to completeParams
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun parseQuery(query: String) = queryCache.getOrPut(query) {
 | 
			
		||||
    fun parseQuery(query: String, enableWildcard: Boolean = true) = queryCache.getOrPut(query) {
 | 
			
		||||
        val res = mutableListOf<QueryComponent>()
 | 
			
		||||
 | 
			
		||||
        var inQuotes = false
 | 
			
		||||
@@ -155,10 +153,10 @@ class SearchEngine {
 | 
			
		||||
        for(char in query.toLowerCase()) {
 | 
			
		||||
            if(char == '"') {
 | 
			
		||||
                inQuotes = !inQuotes
 | 
			
		||||
            } else if(char == '?' || char == '_') {
 | 
			
		||||
            } else if(enableWildcard && (char == '?' || char == '_')) {
 | 
			
		||||
                flushText()
 | 
			
		||||
                queuedText.add(SingleWildcard(char.toString()))
 | 
			
		||||
            } else if(char == '*' || char == '%') {
 | 
			
		||||
            } else if(enableWildcard && (char == '*' || char == '%')) {
 | 
			
		||||
                flushText()
 | 
			
		||||
                queuedText.add(MultiWildcard(char.toString()))
 | 
			
		||||
            } else if(char == '-') {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user