mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Add HBrowse
This commit is contained in:
		| @@ -303,6 +303,15 @@ dependencies { | ||||
|     implementation 'com.github.mfornos:humanize-slim:1.2.2' | ||||
|  | ||||
|     implementation 'com.android.support:gridlayout-v7:28.0.0' | ||||
|  | ||||
|     final def markwon_version = '4.1.0' | ||||
|  | ||||
|     implementation "io.noties.markwon:core:$markwon_version" | ||||
|     implementation "io.noties.markwon:ext-strikethrough:$markwon_version" | ||||
|     implementation "io.noties.markwon:ext-tables:$markwon_version" | ||||
|     implementation "io.noties.markwon:html:$markwon_version" | ||||
|     implementation "io.noties.markwon:image:$markwon_version" | ||||
|     implementation "io.noties.markwon:linkify:$markwon_version" | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|   | ||||
| @@ -257,6 +257,16 @@ | ||||
|                     android:host="pururin.io" | ||||
|                     android:pathPrefix="/gallery/" | ||||
|                     android:scheme="https" /> | ||||
|  | ||||
|                 <!-- HBrowse --> | ||||
|                 <data | ||||
|                     android:host="www.hbrowse.com" | ||||
|                     android:pathPrefix="/" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="www.hbrowse.com" | ||||
|                     android:pathPrefix="/" | ||||
|                     android:scheme="https" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|   | ||||
| @@ -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 == '-') { | ||||
|   | ||||
							
								
								
									
										19
									
								
								app/src/main/res/layout/navigation_view_help_dialog.xml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/res/layout/navigation_view_help_dialog.xml
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="?attr/listPreferredItemHeightSmall" | ||||
|     android:paddingLeft="?attr/listPreferredItemPaddingLeft" | ||||
|     android:paddingRight="?attr/listPreferredItemPaddingRight" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:focusable="true"> | ||||
|  | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/dialog_open_button" | ||||
|         style="@style/Theme.Widget.Button.Borderless" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_weight="1" | ||||
|         android:text="Button" /> | ||||
| </LinearLayout> | ||||
		Reference in New Issue
	
	Block a user