New multisrc theme: Liliana (#2413)
* new multisrc theme: liliana * dont specify type * suggestions * add raw1001
							
								
								
									
										5
									
								
								lib-multisrc/liliana/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | |||||||
|  | plugins { | ||||||
|  |     id("lib-multisrc") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | baseVersionCode = 1 | ||||||
| @ -0,0 +1,90 @@ | |||||||
|  | package eu.kanade.tachiyomi.multisrc.liliana | ||||||
|  | 
 | ||||||
|  | import eu.kanade.tachiyomi.source.model.Filter | ||||||
|  | import okhttp3.HttpUrl | ||||||
|  | 
 | ||||||
|  | interface UrlPartFilter { | ||||||
|  |     fun addUrlParameter(url: HttpUrl.Builder) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | abstract class SelectFilter( | ||||||
|  |     name: String, | ||||||
|  |     private val options: List<Pair<String, String>>, | ||||||
|  |     private val urlParameter: String, | ||||||
|  | ) : UrlPartFilter, Filter.Select<String>( | ||||||
|  |     name, | ||||||
|  |     options.map { it.first }.toTypedArray(), | ||||||
|  | ) { | ||||||
|  |     override fun addUrlParameter(url: HttpUrl.Builder) { | ||||||
|  |         url.addQueryParameter(urlParameter, options[state].second) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class TriStateFilter(name: String, val id: String) : Filter.TriState(name) | ||||||
|  | 
 | ||||||
|  | abstract class TriStateGroupFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  |     private val includeUrlParameter: String, | ||||||
|  |     private val excludeUrlParameter: String, | ||||||
|  | ) : UrlPartFilter, Filter.Group<TriStateFilter>( | ||||||
|  |     name, | ||||||
|  |     options.map { TriStateFilter(it.first, it.second) }, | ||||||
|  | ) { | ||||||
|  |     override fun addUrlParameter(url: HttpUrl.Builder) { | ||||||
|  |         url.addQueryParameter( | ||||||
|  |             includeUrlParameter, | ||||||
|  |             state.filter { it.isIncluded() }.joinToString(",") { it.id }, | ||||||
|  |         ) | ||||||
|  |         url.addQueryParameter( | ||||||
|  |             excludeUrlParameter, | ||||||
|  |             state.filter { it.isExcluded() }.joinToString(",") { it.id }, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class GenreFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  | ) : TriStateGroupFilter( | ||||||
|  |     name, | ||||||
|  |     options, | ||||||
|  |     "genres", | ||||||
|  |     "notGenres", | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | class ChapterCountFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  | ) : SelectFilter( | ||||||
|  |     name, | ||||||
|  |     options, | ||||||
|  |     "chapter_count", | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | class StatusFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  | ) : SelectFilter( | ||||||
|  |     name, | ||||||
|  |     options, | ||||||
|  |     "status", | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | class GenderFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  | ) : SelectFilter( | ||||||
|  |     name, | ||||||
|  |     options, | ||||||
|  |     "sex", | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | class SortFilter( | ||||||
|  |     name: String, | ||||||
|  |     options: List<Pair<String, String>>, | ||||||
|  | ) : SelectFilter( | ||||||
|  |     name, | ||||||
|  |     options, | ||||||
|  |     "sort", | ||||||
|  | ) | ||||||
| @ -0,0 +1,353 @@ | |||||||
|  | package eu.kanade.tachiyomi.multisrc.liliana | ||||||
|  | 
 | ||||||
|  | import android.util.Log | ||||||
|  | import eu.kanade.tachiyomi.network.GET | ||||||
|  | import eu.kanade.tachiyomi.network.POST | ||||||
|  | import eu.kanade.tachiyomi.network.await | ||||||
|  | import eu.kanade.tachiyomi.source.model.Filter | ||||||
|  | import eu.kanade.tachiyomi.source.model.FilterList | ||||||
|  | import eu.kanade.tachiyomi.source.model.MangasPage | ||||||
|  | import eu.kanade.tachiyomi.source.model.Page | ||||||
|  | import eu.kanade.tachiyomi.source.model.SChapter | ||||||
|  | import eu.kanade.tachiyomi.source.model.SManga | ||||||
|  | import eu.kanade.tachiyomi.source.online.ParsedHttpSource | ||||||
|  | import eu.kanade.tachiyomi.util.asJsoup | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlinx.serialization.decodeFromString | ||||||
|  | import kotlinx.serialization.json.Json | ||||||
|  | import okhttp3.FormBody | ||||||
|  | import okhttp3.HttpUrl.Companion.toHttpUrl | ||||||
|  | import okhttp3.Request | ||||||
|  | import okhttp3.Response | ||||||
|  | import org.jsoup.Jsoup | ||||||
|  | import org.jsoup.nodes.Document | ||||||
|  | import org.jsoup.nodes.Element | ||||||
|  | import uy.kohesive.injekt.injectLazy | ||||||
|  | import java.lang.Exception | ||||||
|  | 
 | ||||||
|  | abstract class Liliana( | ||||||
|  |     override val name: String, | ||||||
|  |     override val baseUrl: String, | ||||||
|  |     final override val lang: String, | ||||||
|  |     private val usesPostSearch: Boolean = false, | ||||||
|  | ) : ParsedHttpSource() { | ||||||
|  |     override val supportsLatest = true | ||||||
|  | 
 | ||||||
|  |     private val json: Json by injectLazy() | ||||||
|  | 
 | ||||||
|  |     override val client = network.cloudflareClient | ||||||
|  | 
 | ||||||
|  |     override fun headersBuilder() = super.headersBuilder() | ||||||
|  |         .add("Referer", "$baseUrl/") | ||||||
|  | 
 | ||||||
|  |     // ============================== Popular =============================== | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers) | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaSelector(): String = "div#main div.grid > div" | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { | ||||||
|  |         thumbnail_url = element.selectFirst("img")?.imgAttr() | ||||||
|  |         with(element.selectFirst(".text-center a")!!) { | ||||||
|  |             title = text() | ||||||
|  |             setUrlWithoutDomain(attr("abs:href")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span" | ||||||
|  | 
 | ||||||
|  |     // =============================== Latest =============================== | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesRequest(page: Int): Request = | ||||||
|  |         GET("$baseUrl/all-manga/$page/?sort=last_update&status=0", headers) | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesParse(response: Response): MangasPage = | ||||||
|  |         popularMangaParse(response) | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesSelector(): String = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesFromElement(element: Element): SManga = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesNextPageSelector(): String = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     // =============================== Search =============================== | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||||
|  |         if (query.isNotBlank() && usesPostSearch) { | ||||||
|  |             val formBody = FormBody.Builder() | ||||||
|  |                 .add("search", query) | ||||||
|  |                 .build() | ||||||
|  | 
 | ||||||
|  |             val formHeaders = headersBuilder().apply { | ||||||
|  |                 add("Accept", "application/json, text/javascript, */*; q=0.01") | ||||||
|  |                 add("Host", baseUrl.toHttpUrl().host) | ||||||
|  |                 add("Origin", baseUrl) | ||||||
|  |                 add("X-Requested-With", "XMLHttpRequest") | ||||||
|  |             }.build() | ||||||
|  | 
 | ||||||
|  |             return POST("$baseUrl/ajax/search", formHeaders, formBody) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val url = baseUrl.toHttpUrl().newBuilder().apply { | ||||||
|  |             if (query.isNotBlank()) { | ||||||
|  |                 addPathSegment("search") | ||||||
|  |                 addQueryParameter("keyword", query) | ||||||
|  |             } else { | ||||||
|  |                 addPathSegment("filter") | ||||||
|  |                 filters.filterIsInstance<UrlPartFilter>().forEach { | ||||||
|  |                     it.addUrlParameter(this) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             addPathSegment(page.toString()) | ||||||
|  |             addPathSegment("") | ||||||
|  |         }.build() | ||||||
|  | 
 | ||||||
|  |         return GET(url, headers) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaParse(response: Response): MangasPage { | ||||||
|  |         if (response.request.method == "GET") { | ||||||
|  |             return popularMangaParse(response) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         val mangaList = response.parseAs<SearchResponseDto>().list.map { manga -> | ||||||
|  |             SManga.create().apply { | ||||||
|  |                 setUrlWithoutDomain(manga.url) | ||||||
|  |                 title = manga.name | ||||||
|  |                 thumbnail_url = baseUrl + manga.cover | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return MangasPage(mangaList, false) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Serializable | ||||||
|  |     class SearchResponseDto( | ||||||
|  |         val list: List<MangaDto>, | ||||||
|  |     ) { | ||||||
|  |         @Serializable | ||||||
|  |         class MangaDto( | ||||||
|  |             val cover: String, | ||||||
|  |             val name: String, | ||||||
|  |             val url: String, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaSelector(): String = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaFromElement(element: Element): SManga = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaNextPageSelector(): String = | ||||||
|  |         throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     // =============================== Filters ============================== | ||||||
|  | 
 | ||||||
|  |     protected var genreName = "" | ||||||
|  |     protected var genreData = listOf<Pair<String, String>>() | ||||||
|  |     protected var chapterCountName = "" | ||||||
|  |     protected var chapterCountData = listOf<Pair<String, String>>() | ||||||
|  |     protected var statusName = "" | ||||||
|  |     protected var statusData = listOf<Pair<String, String>>() | ||||||
|  |     protected var genderName = "" | ||||||
|  |     protected var genderData = listOf<Pair<String, String>>() | ||||||
|  |     protected var sortName = "" | ||||||
|  |     protected var sortData = listOf<Pair<String, String>>() | ||||||
|  |     private var fetchFilterAttempts = 0 | ||||||
|  | 
 | ||||||
|  |     protected suspend fun fetchFilters() { | ||||||
|  |         if ( | ||||||
|  |             fetchFilterAttempts < 3 && | ||||||
|  |             arrayOf(genreData, chapterCountData, statusData, genderData, sortData).any { it.isEmpty() } | ||||||
|  |         ) { | ||||||
|  |             try { | ||||||
|  |                 val doc = client.newCall(filtersRequest()) | ||||||
|  |                     .await() | ||||||
|  |                     .asJsoup() | ||||||
|  | 
 | ||||||
|  |                 parseFilters(doc) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Log.e("$name: Filters", e.stackTraceToString()) | ||||||
|  |             } | ||||||
|  |             fetchFilterAttempts++ | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected open fun filtersRequest() = GET("$baseUrl/filter", headers) | ||||||
|  | 
 | ||||||
|  |     protected open fun parseFilters(document: Document) { | ||||||
|  |         genreName = document.selectFirst("div.advanced-genres > h3")?.text() ?: "" | ||||||
|  |         genreData = document.select("div.advanced-genres > div > .advance-item").map { | ||||||
|  |             it.text() to it.selectFirst("span")!!.attr("data-genre") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         chapterCountName = document.getSelectName("select-count") | ||||||
|  |         chapterCountData = document.getSelectData("select-count") | ||||||
|  | 
 | ||||||
|  |         statusName = document.getSelectName("select-status") | ||||||
|  |         statusData = document.getSelectData("select-status") | ||||||
|  | 
 | ||||||
|  |         genderName = document.getSelectName("select-gender") | ||||||
|  |         genderData = document.getSelectData("select-gender") | ||||||
|  | 
 | ||||||
|  |         sortName = document.getSelectName("select-sort") | ||||||
|  |         sortData = document.getSelectData("select-sort") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun Document.getSelectName(selectorClass: String): String { | ||||||
|  |         return this.selectFirst(".select-div > label.$selectorClass")?.text() ?: "" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun Document.getSelectData(selectorId: String): List<Pair<String, String>> { | ||||||
|  |         return this.select("#$selectorId > option").map { | ||||||
|  |             it.text() to it.attr("value") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun getFilterList(): FilterList { | ||||||
|  |         launchIO { fetchFilters() } | ||||||
|  | 
 | ||||||
|  |         val filters = mutableListOf<Filter<*>>() | ||||||
|  | 
 | ||||||
|  |         if (genreData.isNotEmpty()) { | ||||||
|  |             filters.add(GenreFilter(genreName, genreData)) | ||||||
|  |         } | ||||||
|  |         if (chapterCountData.isNotEmpty()) { | ||||||
|  |             filters.add(ChapterCountFilter(chapterCountName, chapterCountData)) | ||||||
|  |         } | ||||||
|  |         if (statusData.isNotEmpty()) { | ||||||
|  |             filters.add(StatusFilter(statusName, statusData)) | ||||||
|  |         } | ||||||
|  |         if (genderData.isNotEmpty()) { | ||||||
|  |             filters.add(GenderFilter(genderName, genderData)) | ||||||
|  |         } | ||||||
|  |         if (sortData.isNotEmpty()) { | ||||||
|  |             filters.add(SortFilter(sortName, sortData)) | ||||||
|  |         } | ||||||
|  |         if (filters.size < 5) { | ||||||
|  |             filters.add(0, Filter.Header("Press 'reset' to load more filters")) | ||||||
|  |         } else { | ||||||
|  |             filters.add(0, Filter.Header("NOTE: Ignored if using text search!")) | ||||||
|  |             filters.add(1, Filter.Separator()) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return FilterList(filters) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private val scope = CoroutineScope(Dispatchers.IO) | ||||||
|  | 
 | ||||||
|  |     protected fun launchIO(block: suspend () -> Unit) = scope.launch { block() } | ||||||
|  | 
 | ||||||
|  |     // =========================== Manga Details ============================ | ||||||
|  | 
 | ||||||
|  |     override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { | ||||||
|  |         description = document.selectFirst("div#syn-target")?.text() | ||||||
|  |         thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr() | ||||||
|  |         title = document.selectFirst(".a2 header h1")!!.text() | ||||||
|  |         genre = document.select(".a2 div > a[rel='tag'].label").joinToString { it.text() } | ||||||
|  |         author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text()?.takeUnless { | ||||||
|  |             it.equals("updating", true) | ||||||
|  |         } | ||||||
|  |         status = document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt").parseStatus() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) { | ||||||
|  |         "ongoing", "đang tiến hành", "進行中" -> SManga.ONGOING | ||||||
|  |         "completed", "hoàn thành", "完了" -> SManga.COMPLETED | ||||||
|  |         "on-hold", "tạm ngưng", "保留" -> SManga.ON_HIATUS | ||||||
|  |         "canceled", "đã huỷ", "キャンセル" -> SManga.CANCELLED | ||||||
|  |         else -> SManga.UNKNOWN | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ============================== Chapters ============================== | ||||||
|  | 
 | ||||||
|  |     override fun chapterListSelector() = "ul > li.chapter" | ||||||
|  | 
 | ||||||
|  |     override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { | ||||||
|  |         element.selectFirst("time[datetime]")?.also { | ||||||
|  |             date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L | ||||||
|  |         } | ||||||
|  |         with(element.selectFirst("a")!!) { | ||||||
|  |             name = text() | ||||||
|  |             setUrlWithoutDomain(attr("abs:href")) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // =============================== Pages ================================ | ||||||
|  | 
 | ||||||
|  |     @Serializable | ||||||
|  |     class PageListResponseDto( | ||||||
|  |         val status: Boolean = false, | ||||||
|  |         val msg: String? = null, | ||||||
|  |         val html: String, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     override fun pageListParse(response: Response): List<Page> { | ||||||
|  |         val document = response.asJsoup() | ||||||
|  |         val script = document.selectFirst("script:containsData(const CHAPTER_ID)")?.data() | ||||||
|  |             ?: throw Exception("Failed to get chapter id") | ||||||
|  | 
 | ||||||
|  |         val chapterId = script.substringAfter("const CHAPTER_ID = ").substringBefore(";") | ||||||
|  | 
 | ||||||
|  |         val pageHeaders = headersBuilder().apply { | ||||||
|  |             add("Accept", "application/json, text/javascript, *//*; q=0.01") | ||||||
|  |             add("Host", baseUrl.toHttpUrl().host) | ||||||
|  |             set("Referer", response.request.url.toString()) | ||||||
|  |             add("X-Requested-With", "XMLHttpRequest") | ||||||
|  |         }.build() | ||||||
|  | 
 | ||||||
|  |         val ajaxResponse = client.newCall( | ||||||
|  |             GET("$baseUrl/ajax/image/list/chap/$chapterId", pageHeaders), | ||||||
|  |         ).execute() | ||||||
|  | 
 | ||||||
|  |         val data = ajaxResponse.parseAs<PageListResponseDto>() | ||||||
|  | 
 | ||||||
|  |         if (!data.status) { | ||||||
|  |             throw Exception(data.msg) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return pageListParse( | ||||||
|  |             Jsoup.parseBodyFragment( | ||||||
|  |                 data.html, | ||||||
|  |                 response.request.url.toString(), | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun pageListParse(document: Document): List<Page> { | ||||||
|  |         return document.select("div.separator").mapIndexed { i, page -> | ||||||
|  |             val url = page.selectFirst("a")!!.attr("abs:href") | ||||||
|  |             Page(i, document.location(), url) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun imageUrlParse(document: Document) = "" | ||||||
|  | 
 | ||||||
|  |     override fun imageRequest(page: Page): Request { | ||||||
|  |         val imgHeaders = headersBuilder().apply { | ||||||
|  |             add("Accept", "image/avif,image/webp,*/*") | ||||||
|  |             add("Host", page.imageUrl!!.toHttpUrl().host) | ||||||
|  |         }.build() | ||||||
|  |         return GET(page.imageUrl!!, imgHeaders) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ============================= Utilities ============================== | ||||||
|  | 
 | ||||||
|  |     // From mangathemesia | ||||||
|  |     private fun Element.imgAttr(): String = when { | ||||||
|  |         hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") | ||||||
|  |         hasAttr("data-src") -> attr("abs:data-src") | ||||||
|  |         else -> attr("abs:src") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private inline fun <reified T> Response.parseAs(): T { | ||||||
|  |         return json.decodeFromString(body.string()) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,9 +1,9 @@ | |||||||
| ext { | ext { | ||||||
|     extName = 'Manhuagold' |     extName = 'Manhuagold' | ||||||
|     extClass = '.Manhuagold' |     extClass = '.Manhuagold' | ||||||
|     themePkg = 'mangareader' |     themePkg = 'liliana' | ||||||
|     baseUrl = 'https://manhuagold.com' |     baseUrl = 'https://manhuagold.top' | ||||||
|     overrideVersionCode = 33 |     overrideVersionCode = 34 | ||||||
|     isNsfw = true |     isNsfw = true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,233 +1,18 @@ | |||||||
| package eu.kanade.tachiyomi.extension.en.comickiba | package eu.kanade.tachiyomi.extension.en.comickiba | ||||||
| 
 | 
 | ||||||
| import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader | import eu.kanade.tachiyomi.multisrc.liliana.Liliana | ||||||
| import eu.kanade.tachiyomi.network.GET |  | ||||||
| import eu.kanade.tachiyomi.network.asObservableSuccess |  | ||||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimit | import eu.kanade.tachiyomi.network.interceptor.rateLimit | ||||||
| import eu.kanade.tachiyomi.source.model.FilterList |  | ||||||
| import eu.kanade.tachiyomi.source.model.Page |  | ||||||
| import eu.kanade.tachiyomi.source.model.SChapter |  | ||||||
| import eu.kanade.tachiyomi.source.model.SManga |  | ||||||
| import eu.kanade.tachiyomi.util.asJsoup |  | ||||||
| import kotlinx.serialization.json.Json |  | ||||||
| import kotlinx.serialization.json.jsonObject |  | ||||||
| import kotlinx.serialization.json.jsonPrimitive |  | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrl |  | ||||||
| import okhttp3.Request |  | ||||||
| import okhttp3.Response |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
| import org.jsoup.nodes.Document |  | ||||||
| import org.jsoup.nodes.Element |  | ||||||
| import org.jsoup.nodes.TextNode |  | ||||||
| import org.jsoup.select.Evaluator |  | ||||||
| import rx.Observable |  | ||||||
| 
 | 
 | ||||||
| class Manhuagold : MangaReader() { | class Manhuagold : Liliana( | ||||||
|  |     "Manhuagold", | ||||||
|  |     "https://manhuagold.top", | ||||||
|  |     "en", | ||||||
|  |     usesPostSearch = true, | ||||||
|  | ) { | ||||||
|  |     // MangaReader -> Liliana | ||||||
|  |     override val versionId = 2 | ||||||
| 
 | 
 | ||||||
|     override val name = "Manhuagold" |     override val client = super.client.newBuilder() | ||||||
| 
 |  | ||||||
|     override val lang = "en" |  | ||||||
| 
 |  | ||||||
|     override val baseUrl = "https://manhuagold.com" |  | ||||||
| 
 |  | ||||||
|     override val client = network.cloudflareClient.newBuilder() |  | ||||||
|         .rateLimit(2) |         .rateLimit(2) | ||||||
|         .build() |         .build() | ||||||
| 
 |  | ||||||
|     override fun headersBuilder() = super.headersBuilder() |  | ||||||
|         .add("Referer", "$baseUrl/") |  | ||||||
| 
 |  | ||||||
|     // Popular |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaRequest(page: Int) = |  | ||||||
|         GET("$baseUrl/filter/$page/?sort=views&sex=All&chapter_count=0", headers) |  | ||||||
| 
 |  | ||||||
|     // Latest |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesRequest(page: Int) = |  | ||||||
|         GET("$baseUrl/filter/$page/?sort=latest-updated&sex=All&chapter_count=0", headers) |  | ||||||
| 
 |  | ||||||
|     // Search |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { |  | ||||||
|         val urlBuilder = baseUrl.toHttpUrl().newBuilder() |  | ||||||
|         if (query.isNotBlank()) { |  | ||||||
|             urlBuilder.addPathSegment("search").apply { |  | ||||||
|                 addQueryParameter("keyword", query) |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             urlBuilder.addPathSegment("filter").apply { |  | ||||||
|                 filters.ifEmpty(::getFilterList).forEach { filter -> |  | ||||||
|                     when (filter) { |  | ||||||
|                         is Select -> { |  | ||||||
|                             addQueryParameter(filter.param, filter.selection) |  | ||||||
|                         } |  | ||||||
|                         is GenresFilter -> { |  | ||||||
|                             addQueryParameter(filter.param, filter.selection) |  | ||||||
|                         } |  | ||||||
|                         else -> {} |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         urlBuilder.addPathSegment(page.toString()) |  | ||||||
|         urlBuilder.addPathSegment("") |  | ||||||
| 
 |  | ||||||
|         return GET(urlBuilder.build(), headers) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaSelector() = ".manga_list-sbs .manga-poster" |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaFromElement(element: Element) = |  | ||||||
|         SManga.create().apply { |  | ||||||
|             setUrlWithoutDomain(element.attr("href")) |  | ||||||
|             element.selectFirst(Evaluator.Tag("img"))!!.let { |  | ||||||
|                 title = it.attr("alt") |  | ||||||
|                 thumbnail_url = it.imgAttr() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaNextPageSelector() = "ul.pagination > li.active + li" |  | ||||||
| 
 |  | ||||||
|     // Filters |  | ||||||
| 
 |  | ||||||
|     override fun getFilterList() = |  | ||||||
|         FilterList( |  | ||||||
|             Note, |  | ||||||
|             StatusFilter(), |  | ||||||
|             SortFilter(), |  | ||||||
|             GenresFilter(), |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     // Details |  | ||||||
| 
 |  | ||||||
|     override fun mangaDetailsParse(document: Document) = SManga.create().apply { |  | ||||||
|         val root = document.selectFirst(Evaluator.Id("ani_detail"))!! |  | ||||||
|         val mangaTitle = root.selectFirst(Evaluator.Class("manga-name"))!!.ownText() |  | ||||||
|         title = mangaTitle |  | ||||||
|         description = root.run { |  | ||||||
|             val description = selectFirst(Evaluator.Class("description"))!!.ownText() |  | ||||||
|             when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) { |  | ||||||
|                 "", mangaTitle -> description |  | ||||||
|                 else -> "$description\n\nAlternative Title: $altTitle" |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.imgAttr() |  | ||||||
|         genre = root.selectFirst(Evaluator.Class("genres"))!!.children().joinToString { it.ownText() } |  | ||||||
|         for (item in root.selectFirst(Evaluator.Class("anisc-info"))!!.children()) { |  | ||||||
|             if (item.hasClass("item").not()) continue |  | ||||||
|             when (item.selectFirst(Evaluator.Class("item-head"))!!.ownText()) { |  | ||||||
|                 "Authors:" -> item.parseAuthorsTo(this) |  | ||||||
|                 "Status:" -> status = when (item.selectFirst(Evaluator.Class("name"))!!.ownText().lowercase()) { |  | ||||||
|                     "ongoing" -> SManga.ONGOING |  | ||||||
|                     "completed" -> SManga.COMPLETED |  | ||||||
|                     "on-hold" -> SManga.ON_HIATUS |  | ||||||
|                     "canceled" -> SManga.CANCELLED |  | ||||||
|                     else -> SManga.UNKNOWN |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun Element.parseAuthorsTo(manga: SManga) { |  | ||||||
|         val authors = select(Evaluator.Tag("a")) |  | ||||||
|         val text = authors.map { it.ownText().replace(",", "") } |  | ||||||
|         val count = authors.size |  | ||||||
|         when (count) { |  | ||||||
|             0 -> return |  | ||||||
|             1 -> { |  | ||||||
|                 manga.author = text[0] |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         val authorList = ArrayList<String>(count) |  | ||||||
|         val artistList = ArrayList<String>(count) |  | ||||||
|         for ((index, author) in authors.withIndex()) { |  | ||||||
|             val textNode = author.nextSibling() as? TextNode |  | ||||||
|             val list = if (textNode != null && "(Art)" in textNode.wholeText) artistList else authorList |  | ||||||
|             list.add(text[index]) |  | ||||||
|         } |  | ||||||
|         if (authorList.isEmpty().not()) manga.author = authorList.joinToString() |  | ||||||
|         if (artistList.isEmpty().not()) manga.artist = artistList.joinToString() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Chapters |  | ||||||
| 
 |  | ||||||
|     override fun chapterListRequest(mangaUrl: String, type: String): Request = |  | ||||||
|         GET(baseUrl + mangaUrl, headers) |  | ||||||
| 
 |  | ||||||
|     override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> { |  | ||||||
|         TODO("Not yet implemented") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override val chapterType = "" |  | ||||||
|     override val volumeType = "" |  | ||||||
| 
 |  | ||||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { |  | ||||||
|         return client.newCall(chapterListRequest(manga)) |  | ||||||
|             .asObservableSuccess() |  | ||||||
|             .map(::parseChapterList) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun parseChapterList(response: Response): List<SChapter> { |  | ||||||
|         val document = response.use { it.asJsoup() } |  | ||||||
| 
 |  | ||||||
|         return document.select(chapterListSelector()) |  | ||||||
|             .map(::chapterFromElement) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun chapterListSelector(): String = "#chapters-list > li" |  | ||||||
| 
 |  | ||||||
|     private fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { |  | ||||||
|         element.selectFirst("a")!!.run { |  | ||||||
|             setUrlWithoutDomain(attr("href")) |  | ||||||
|             name = selectFirst(".name")?.text() ?: text() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Images |  | ||||||
| 
 |  | ||||||
|     override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable { |  | ||||||
|         val document = client.newCall(pageListRequest(chapter)).execute().asJsoup() |  | ||||||
| 
 |  | ||||||
|         val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data() |  | ||||||
|         val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";") |  | ||||||
| 
 |  | ||||||
|         val ajaxHeaders = super.headersBuilder().apply { |  | ||||||
|             add("Accept", "application/json, text/javascript, */*; q=0.01") |  | ||||||
|             add("Referer", baseUrl + chapter.url) |  | ||||||
|             add("X-Requested-With", "XMLHttpRequest") |  | ||||||
|         }.build() |  | ||||||
| 
 |  | ||||||
|         val ajaxUrl = "$baseUrl/ajax/image/list/chap/$id" |  | ||||||
|         client.newCall(GET(ajaxUrl, ajaxHeaders)).execute().let(::pageListParse) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun pageListParse(response: Response): List<Page> { |  | ||||||
|         val document = response.use { it.parseHtmlProperty() } |  | ||||||
| 
 |  | ||||||
|         val pageList = document.select("div").map { |  | ||||||
|             val index = it.attr("data-number").toInt() |  | ||||||
|             val imgUrl = it.imgAttr().ifEmpty { it.selectFirst("img")!!.imgAttr() } |  | ||||||
| 
 |  | ||||||
|             Page(index, "", imgUrl) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return pageList |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Utilities |  | ||||||
| 
 |  | ||||||
|     // From mangathemesia |  | ||||||
|     private fun Element.imgAttr(): String = when { |  | ||||||
|         hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") |  | ||||||
|         hasAttr("data-src") -> attr("abs:data-src") |  | ||||||
|         else -> attr("abs:src") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun Response.parseHtmlProperty(): Document { |  | ||||||
|         val html = Json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content |  | ||||||
|         return Jsoup.parseBodyFragment(html) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| ext { | ext { | ||||||
|     extName = 'ManhuaPlus (unoriginal)' |     extName = 'ManhuaPlus (unoriginal)' | ||||||
|     extClass = '.ManhuaPlusOrg' |     extClass = '.ManhuaPlusOrg' | ||||||
|     extVersionCode = 1 |     themePkg = 'liliana' | ||||||
|  |     baseUrl = 'https://manhuaplus.org' | ||||||
|  |     overrideVersionCode = 1 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| apply from: "$rootDir/common.gradle" | apply from: "$rootDir/common.gradle" | ||||||
|  | |||||||
| @ -1,242 +1,9 @@ | |||||||
| package eu.kanade.tachiyomi.extension.en.manhuaplusorg | package eu.kanade.tachiyomi.extension.en.manhuaplusorg | ||||||
| 
 | 
 | ||||||
| import eu.kanade.tachiyomi.network.GET | import eu.kanade.tachiyomi.multisrc.liliana.Liliana | ||||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimit |  | ||||||
| import eu.kanade.tachiyomi.source.model.Filter |  | ||||||
| import eu.kanade.tachiyomi.source.model.FilterList |  | ||||||
| import eu.kanade.tachiyomi.source.model.MangasPage |  | ||||||
| import eu.kanade.tachiyomi.source.model.Page |  | ||||||
| import eu.kanade.tachiyomi.source.model.SChapter |  | ||||||
| import eu.kanade.tachiyomi.source.model.SManga |  | ||||||
| import eu.kanade.tachiyomi.source.online.ParsedHttpSource |  | ||||||
| import eu.kanade.tachiyomi.util.asJsoup |  | ||||||
| import kotlinx.serialization.Serializable |  | ||||||
| import kotlinx.serialization.decodeFromString |  | ||||||
| import kotlinx.serialization.json.Json |  | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrl |  | ||||||
| import okhttp3.OkHttpClient |  | ||||||
| import okhttp3.Request |  | ||||||
| import okhttp3.Response |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
| import org.jsoup.nodes.Document |  | ||||||
| import org.jsoup.nodes.Element |  | ||||||
| import uy.kohesive.injekt.injectLazy |  | ||||||
| 
 | 
 | ||||||
| class ManhuaPlusOrg : ParsedHttpSource() { | class ManhuaPlusOrg : Liliana( | ||||||
| 
 |     "ManhuaPlus (Unoriginal)", | ||||||
|     override val name = "ManhuaPlus (Unoriginal)" |     "https://manhuaplus.org", | ||||||
| 
 |     "en", | ||||||
|     override val baseUrl = "https://manhuaplus.org" | ) | ||||||
| 
 |  | ||||||
|     override val lang = "en" |  | ||||||
| 
 |  | ||||||
|     override val supportsLatest = true |  | ||||||
| 
 |  | ||||||
|     private val json: Json by injectLazy() |  | ||||||
| 
 |  | ||||||
|     override val client: OkHttpClient = network.cloudflareClient.newBuilder() |  | ||||||
|         .rateLimit(1) |  | ||||||
|         .build() |  | ||||||
| 
 |  | ||||||
|     override fun headersBuilder() = super.headersBuilder() |  | ||||||
|         .add("Referer", "$baseUrl/") |  | ||||||
| 
 |  | ||||||
|     // Popular |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/week/$page", headers) |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaSelector(): String = "div#main div.grid > div" |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { |  | ||||||
|         thumbnail_url = element.selectFirst("img")?.imgAttr() |  | ||||||
|         element.selectFirst(".text-center a")!!.run { |  | ||||||
|             title = text().trim() |  | ||||||
|             setUrlWithoutDomain(attr("href")) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaNextPageSelector(): String = ".blog-pager > span.pagecurrent + span" |  | ||||||
| 
 |  | ||||||
|     // Latest |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesRequest(page: Int): Request = |  | ||||||
|         GET("$baseUrl/all-manga/$page/?sort=1", headers) |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesSelector(): String = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesFromElement(element: Element): SManga = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesNextPageSelector(): String = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     // Search |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { |  | ||||||
|         val url = baseUrl.toHttpUrl().newBuilder().apply { |  | ||||||
|             if (query.isNotBlank()) { |  | ||||||
|                 addPathSegment("search") |  | ||||||
|                 addQueryParameter("keyword", query) |  | ||||||
|             } else { |  | ||||||
|                 addPathSegment("filter") |  | ||||||
|                 filters.forEach { filter -> |  | ||||||
|                     when (filter) { |  | ||||||
|                         is GenreFilter -> { |  | ||||||
|                             if (filter.checked.isNotEmpty()) { |  | ||||||
|                                 addQueryParameter("genres", filter.checked.joinToString(",")) |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         is StatusFilter -> { |  | ||||||
|                             if (filter.selected.isNotBlank()) { |  | ||||||
|                                 addQueryParameter("status", filter.selected) |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         is SortFilter -> { |  | ||||||
|                             addQueryParameter("sort", filter.selected) |  | ||||||
|                         } |  | ||||||
|                         is ChapterCountFilter -> { |  | ||||||
|                             addQueryParameter("chapter_count", filter.selected) |  | ||||||
|                         } |  | ||||||
|                         is GenderFilter -> { |  | ||||||
|                             addQueryParameter("sex", filter.selected) |  | ||||||
|                         } |  | ||||||
|                         else -> {} |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             addPathSegment(page.toString()) |  | ||||||
|             addPathSegment("") |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return GET(url.build(), headers) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaSelector(): String = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaFromElement(element: Element): SManga = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaNextPageSelector(): String = |  | ||||||
|         throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     // Filters |  | ||||||
| 
 |  | ||||||
|     override fun getFilterList(): FilterList = FilterList( |  | ||||||
|         Filter.Header("Ignored when using text search"), |  | ||||||
|         Filter.Separator(), |  | ||||||
|         GenreFilter(), |  | ||||||
|         ChapterCountFilter(), |  | ||||||
|         GenderFilter(), |  | ||||||
|         StatusFilter(), |  | ||||||
|         SortFilter(), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     // Details |  | ||||||
| 
 |  | ||||||
|     override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { |  | ||||||
|         description = document.selectFirst("div#syn-target")?.text() |  | ||||||
|         thumbnail_url = document.selectFirst(".a1 > figure img")?.imgAttr() |  | ||||||
|         title = document.selectFirst(".a2 header h1")?.text()?.trim() ?: "N/A" |  | ||||||
|         genre = document.select(".a2 div > a[rel='tag'].label").joinToString(", ") { it.text() } |  | ||||||
| 
 |  | ||||||
|         document.selectFirst(".a1 > aside")?.run { |  | ||||||
|             author = select("div:contains(Authors) > span a") |  | ||||||
|                 .joinToString(", ") { it.text().trim() } |  | ||||||
|                 .takeUnless { it.isBlank() || it.equals("Updating", true) } |  | ||||||
|             status = selectFirst("div:contains(Status) > span")?.text().let(::parseStatus) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private fun parseStatus(status: String?): Int = when { |  | ||||||
|         status.equals("ongoing", true) -> SManga.ONGOING |  | ||||||
|         status.equals("completed", true) -> SManga.COMPLETED |  | ||||||
|         status.equals("on-hold", true) -> SManga.ON_HIATUS |  | ||||||
|         status.equals("canceled", true) -> SManga.CANCELLED |  | ||||||
|         else -> SManga.UNKNOWN |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Chapters |  | ||||||
| 
 |  | ||||||
|     override fun chapterListSelector() = "ul > li.chapter" |  | ||||||
| 
 |  | ||||||
|     override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { |  | ||||||
|         element.selectFirst("time[datetime]")?.also { |  | ||||||
|             date_upload = it.attr("datetime").toLongOrNull()?.let { it * 1000L } ?: 0L |  | ||||||
|         } |  | ||||||
|         element.selectFirst("a")!!.run { |  | ||||||
|             text().trim().also { |  | ||||||
|                 name = it |  | ||||||
|                 chapter_number = it.substringAfter("hapter ").toFloatOrNull() ?: 0F |  | ||||||
|             } |  | ||||||
|             setUrlWithoutDomain(attr("href")) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun pageListRequest(chapter: SChapter): Request { |  | ||||||
|         val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup() |  | ||||||
| 
 |  | ||||||
|         val script = document.selectFirst("script:containsData(const CHAPTER_ID)")!!.data() |  | ||||||
| 
 |  | ||||||
|         val id = script.substringAfter("const CHAPTER_ID = ").substringBefore(";") |  | ||||||
| 
 |  | ||||||
|         val pageHeaders = headersBuilder().apply { |  | ||||||
|             add("Accept", "application/json, text/javascript, *//*; q=0.01") |  | ||||||
|             add("Host", baseUrl.toHttpUrl().host) |  | ||||||
|             add("Referer", baseUrl + chapter.url) |  | ||||||
|             add("X-Requested-With", "XMLHttpRequest") |  | ||||||
|         }.build() |  | ||||||
| 
 |  | ||||||
|         return GET("$baseUrl/ajax/image/list/chap/$id", pageHeaders) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Serializable |  | ||||||
|     data class PageListResponseDto(val html: String) |  | ||||||
| 
 |  | ||||||
|     override fun pageListParse(response: Response): List<Page> { |  | ||||||
|         val data = response.parseAs<PageListResponseDto>().html |  | ||||||
|         return pageListParse( |  | ||||||
|             Jsoup.parseBodyFragment( |  | ||||||
|                 data, |  | ||||||
|                 response.request.header("Referer")!!, |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun pageListParse(document: Document): List<Page> { |  | ||||||
|         return document.select("div.separator").map { page -> |  | ||||||
|             val index = page.selectFirst("img")!!.attr("alt").substringAfterLast(" ").toInt() |  | ||||||
|             val url = page.selectFirst("a")!!.attr("abs:href") |  | ||||||
|             Page(index, document.location(), url) |  | ||||||
|         }.sortedBy { it.index } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun imageUrlParse(document: Document) = "" |  | ||||||
| 
 |  | ||||||
|     override fun imageRequest(page: Page): Request { |  | ||||||
|         val imgHeaders = headersBuilder().apply { |  | ||||||
|             add("Accept", "image/avif,image/webp,*/*") |  | ||||||
|             add("Host", page.imageUrl!!.toHttpUrl().host) |  | ||||||
|         }.build() |  | ||||||
|         return GET(page.imageUrl!!, imgHeaders) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Utilities |  | ||||||
| 
 |  | ||||||
|     // From mangathemesia |  | ||||||
|     private fun Element.imgAttr(): String = when { |  | ||||||
|         hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") |  | ||||||
|         hasAttr("data-src") -> attr("abs:data-src") |  | ||||||
|         else -> attr("abs:src") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private inline fun <reified T> Response.parseAs(): T { |  | ||||||
|         return json.decodeFromString(body.string()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,139 +0,0 @@ | |||||||
| package eu.kanade.tachiyomi.extension.en.manhuaplusorg |  | ||||||
| 
 |  | ||||||
| import eu.kanade.tachiyomi.source.model.Filter |  | ||||||
| 
 |  | ||||||
| abstract class SelectFilter( |  | ||||||
|     name: String, |  | ||||||
|     private val options: List<Pair<String, String>>, |  | ||||||
| ) : Filter.Select<String>( |  | ||||||
|     name, |  | ||||||
|     options.map { it.first }.toTypedArray(), |  | ||||||
| ) { |  | ||||||
|     val selected get() = options[state].second |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class CheckBoxFilter( |  | ||||||
|     name: String, |  | ||||||
|     val value: String, |  | ||||||
| ) : Filter.CheckBox(name) |  | ||||||
| 
 |  | ||||||
| class ChapterCountFilter : SelectFilter("Chapter count", chapterCount) { |  | ||||||
|     companion object { |  | ||||||
|         private val chapterCount = listOf( |  | ||||||
|             Pair(">= 0", "0"), |  | ||||||
|             Pair(">= 10", "10"), |  | ||||||
|             Pair(">= 30", "30"), |  | ||||||
|             Pair(">= 50", "50"), |  | ||||||
|             Pair(">= 100", "100"), |  | ||||||
|             Pair(">= 200", "200"), |  | ||||||
|             Pair(">= 300", "300"), |  | ||||||
|             Pair(">= 400", "400"), |  | ||||||
|             Pair(">= 500", "500"), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class GenderFilter : SelectFilter("Manga Gender", gender) { |  | ||||||
|     companion object { |  | ||||||
|         private val gender = listOf( |  | ||||||
|             Pair("All", "All"), |  | ||||||
|             Pair("Boy", "Boy"), |  | ||||||
|             Pair("Girl", "Girl"), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class StatusFilter : SelectFilter("Status", status) { |  | ||||||
|     companion object { |  | ||||||
|         private val status = listOf( |  | ||||||
|             Pair("All", ""), |  | ||||||
|             Pair("Completed", "completed"), |  | ||||||
|             Pair("OnGoing", "on-going"), |  | ||||||
|             Pair("On-Hold", "on-hold"), |  | ||||||
|             Pair("Canceled", "canceled"), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class SortFilter : SelectFilter("Sort", sort) { |  | ||||||
|     companion object { |  | ||||||
|         private val sort = listOf( |  | ||||||
|             Pair("Default", "default"), |  | ||||||
|             Pair("Latest Updated", "latest-updated"), |  | ||||||
|             Pair("Most Viewed", "views"), |  | ||||||
|             Pair("Most Viewed Month", "views_month"), |  | ||||||
|             Pair("Most Viewed Week", "views_week"), |  | ||||||
|             Pair("Most Viewed Day", "views_day"), |  | ||||||
|             Pair("Score", "score"), |  | ||||||
|             Pair("Name A-Z", "az"), |  | ||||||
|             Pair("Name Z-A", "za"), |  | ||||||
|             Pair("Newest", "new"), |  | ||||||
|             Pair("Oldest", "old"), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class GenreFilter : Filter.Group<CheckBoxFilter>( |  | ||||||
|     "Genre", |  | ||||||
|     genres.map { CheckBoxFilter(it.first, it.second) }, |  | ||||||
| ) { |  | ||||||
|     val checked get() = state.filter { it.state }.map { it.value } |  | ||||||
| 
 |  | ||||||
|     companion object { |  | ||||||
|         private val genres = listOf( |  | ||||||
|             Pair("Action", "4"), |  | ||||||
|             Pair("Adaptation", "87"), |  | ||||||
|             Pair("Adult", "31"), |  | ||||||
|             Pair("Adventure", "5"), |  | ||||||
|             Pair("Animals", "1657"), |  | ||||||
|             Pair("Cartoon", "46"), |  | ||||||
|             Pair("Comedy", "14"), |  | ||||||
|             Pair("Demons", "284"), |  | ||||||
|             Pair("Drama", "59"), |  | ||||||
|             Pair("Ecchi", "67"), |  | ||||||
|             Pair("Fantasy", "6"), |  | ||||||
|             Pair("Full Color", "89"), |  | ||||||
|             Pair("Genderswap", "2409"), |  | ||||||
|             Pair("Ghosts", "2253"), |  | ||||||
|             Pair("Gore", "1182"), |  | ||||||
|             Pair("Harem", "17"), |  | ||||||
|             Pair("Historical", "642"), |  | ||||||
|             Pair("Horror", "797"), |  | ||||||
|             Pair("Isekai", "239"), |  | ||||||
|             Pair("Live action", "11"), |  | ||||||
|             Pair("Long Strip", "86"), |  | ||||||
|             Pair("Magic", "90"), |  | ||||||
|             Pair("Magical Girls", "1470"), |  | ||||||
|             Pair("Manhua", "7"), |  | ||||||
|             Pair("Manhwa", "70"), |  | ||||||
|             Pair("Martial Arts", "8"), |  | ||||||
|             Pair("Mature", "12"), |  | ||||||
|             Pair("Mecha", "786"), |  | ||||||
|             Pair("Medical", "1443"), |  | ||||||
|             Pair("Monsters", "138"), |  | ||||||
|             Pair("Mystery", "9"), |  | ||||||
|             Pair("Post-Apocalyptic", "285"), |  | ||||||
|             Pair("Psychological", "798"), |  | ||||||
|             Pair("Reincarnation", "139"), |  | ||||||
|             Pair("Romance", "987"), |  | ||||||
|             Pair("School Life", "10"), |  | ||||||
|             Pair("Sci-fi", "135"), |  | ||||||
|             Pair("Seinen", "196"), |  | ||||||
|             Pair("Shounen", "26"), |  | ||||||
|             Pair("Shounen ai", "64"), |  | ||||||
|             Pair("Slice of Life", "197"), |  | ||||||
|             Pair("Superhero", "136"), |  | ||||||
|             Pair("Supernatural", "13"), |  | ||||||
|             Pair("Survival", "140"), |  | ||||||
|             Pair("Thriller", "137"), |  | ||||||
|             Pair("Time travel", "231"), |  | ||||||
|             Pair("Tragedy", "15"), |  | ||||||
|             Pair("Video Games", "283"), |  | ||||||
|             Pair("Villainess", "676"), |  | ||||||
|             Pair("Virtual Reality", "611"), |  | ||||||
|             Pair("Web comic", "88"), |  | ||||||
|             Pair("Webtoon", "18"), |  | ||||||
|             Pair("Wuxia", "239"), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										9
									
								
								src/ja/mangakoma/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | |||||||
|  | ext { | ||||||
|  |     extName = 'Manga Koma' | ||||||
|  |     extClass = '.MangaKoma' | ||||||
|  |     themePkg = 'liliana' | ||||||
|  |     baseUrl = 'https://mangakoma01.net' | ||||||
|  |     overrideVersionCode = 0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | apply from: "$rootDir/common.gradle" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/ja/mangakoma/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/mangakoma/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/mangakoma/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/mangakoma/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/mangakoma/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.6 KiB | 
| @ -0,0 +1,15 @@ | |||||||
|  | package eu.kanade.tachiyomi.extension.ja.mangakoma | ||||||
|  | 
 | ||||||
|  | import eu.kanade.tachiyomi.multisrc.liliana.Liliana | ||||||
|  | import eu.kanade.tachiyomi.source.model.Page | ||||||
|  | import org.jsoup.nodes.Document | ||||||
|  | 
 | ||||||
|  | class MangaKoma : Liliana("Manga Koma", "https://mangakoma01.net", "ja") { | ||||||
|  |     override fun pageListParse(document: Document): List<Page> { | ||||||
|  |         return document.select("div.separator[data-index]").map { page -> | ||||||
|  |             val index = page.attr("data-index").toInt() | ||||||
|  |             val url = page.selectFirst("a")!!.attr("abs:href") | ||||||
|  |             Page(index, document.location(), url) | ||||||
|  |         }.sortedBy { it.index } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/ja/raw1001/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | |||||||
|  | ext { | ||||||
|  |     extName = 'Raw1001' | ||||||
|  |     extClass = '.Raw1001' | ||||||
|  |     themePkg = 'liliana' | ||||||
|  |     baseUrl = 'https://raw1001.net' | ||||||
|  |     overrideVersionCode = 0 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | apply from: "$rootDir/common.gradle" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/ja/raw1001/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/raw1001/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/raw1001/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/raw1001/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/ja/raw1001/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.1 KiB | 
| @ -0,0 +1,5 @@ | |||||||
|  | package eu.kanade.tachiyomi.extension.ja.raw1001 | ||||||
|  | 
 | ||||||
|  | import eu.kanade.tachiyomi.multisrc.liliana.Liliana | ||||||
|  | 
 | ||||||
|  | class Raw1001 : Liliana("Raw1001", "https://raw1001.net", "ja") | ||||||
| @ -1,7 +1,9 @@ | |||||||
| ext { | ext { | ||||||
|     extName = 'DocTruyen5s' |     extName = 'DocTruyen5s' | ||||||
|     extClass = '.DocTruyen5s' |     extClass = '.DocTruyen5s' | ||||||
|     extVersionCode = 2 |     themePkg = 'liliana' | ||||||
|  |     baseUrl = 'https://manga.io.vn' | ||||||
|  |     overrideVersionCode = 2 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| apply from: "$rootDir/common.gradle" | apply from: "$rootDir/common.gradle" | ||||||
|  | |||||||
| @ -1,377 +1,5 @@ | |||||||
| package eu.kanade.tachiyomi.extension.vi.doctruyen5s | package eu.kanade.tachiyomi.extension.vi.doctruyen5s | ||||||
| 
 | 
 | ||||||
| import eu.kanade.tachiyomi.network.GET | import eu.kanade.tachiyomi.multisrc.liliana.Liliana | ||||||
| import eu.kanade.tachiyomi.network.POST |  | ||||||
| import eu.kanade.tachiyomi.source.model.Filter |  | ||||||
| import eu.kanade.tachiyomi.source.model.FilterList |  | ||||||
| import eu.kanade.tachiyomi.source.model.Page |  | ||||||
| import eu.kanade.tachiyomi.source.model.SChapter |  | ||||||
| import eu.kanade.tachiyomi.source.model.SManga |  | ||||||
| import eu.kanade.tachiyomi.source.online.ParsedHttpSource |  | ||||||
| import kotlinx.serialization.Serializable |  | ||||||
| import kotlinx.serialization.decodeFromString |  | ||||||
| import kotlinx.serialization.json.Json |  | ||||||
| import okhttp3.FormBody |  | ||||||
| import okhttp3.HttpUrl |  | ||||||
| import okhttp3.HttpUrl.Companion.toHttpUrl |  | ||||||
| import okhttp3.Request |  | ||||||
| import okhttp3.Response |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
| import org.jsoup.nodes.Document |  | ||||||
| import org.jsoup.nodes.Element |  | ||||||
| import uy.kohesive.injekt.injectLazy |  | ||||||
| 
 | 
 | ||||||
| class DocTruyen5s : ParsedHttpSource() { | class DocTruyen5s : Liliana("DocTruyen5s", "https://manga.io.vn", "vi") | ||||||
| 
 |  | ||||||
|     override val name = "DocTruyen5s" |  | ||||||
| 
 |  | ||||||
|     override val lang = "vi" |  | ||||||
| 
 |  | ||||||
|     override val baseUrl = "https://manga.io.vn" |  | ||||||
| 
 |  | ||||||
|     override val supportsLatest = true |  | ||||||
| 
 |  | ||||||
|     override val client = network.cloudflareClient |  | ||||||
| 
 |  | ||||||
|     private val json: Json by injectLazy() |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaRequest(page: Int) = |  | ||||||
|         GET("$baseUrl/filter/$page/?sort=views_day&chapter_count=0&sex=All", headers) |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaSelector() = "div.Blog section div.grid > div" |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaFromElement(element: Element) = SManga.create().apply { |  | ||||||
|         val anchor = element.selectFirst("div.text-center a")!! |  | ||||||
| 
 |  | ||||||
|         setUrlWithoutDomain(anchor.attr("abs:href")) |  | ||||||
|         title = anchor.text() |  | ||||||
|         thumbnail_url = element.selectFirst("img")?.attr("abs:data-src") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun popularMangaNextPageSelector() = "span.pagecurrent:not(:last-child)" |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesRequest(page: Int) = |  | ||||||
|         GET("$baseUrl/filter/$page/?sort=latest-updated&chapter_count=0&sex=All", headers) |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesSelector() = popularMangaSelector() |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) |  | ||||||
| 
 |  | ||||||
|     override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { |  | ||||||
|         val url = if (query.isNotBlank()) { |  | ||||||
|             "$baseUrl/search/$page/".toHttpUrl().newBuilder().apply { |  | ||||||
|                 addQueryParameter("keyword", query) |  | ||||||
|             }.build() |  | ||||||
|         } else { |  | ||||||
|             val builder = "$baseUrl/filter/$page/".toHttpUrl().newBuilder() |  | ||||||
| 
 |  | ||||||
|             (if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<UriFilter>() |  | ||||||
|                 .forEach { it.addToUri(builder) } |  | ||||||
| 
 |  | ||||||
|             builder.build() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return GET(url, headers) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaSelector() = popularMangaSelector() |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) |  | ||||||
| 
 |  | ||||||
|     override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() |  | ||||||
| 
 |  | ||||||
|     override fun mangaDetailsParse(document: Document) = SManga.create().apply { |  | ||||||
|         title = document.selectFirst("article header h1")!!.text() |  | ||||||
|         author = document.selectFirst("div.y6x11p i.fas.fa-user + span.dt")?.text() |  | ||||||
|         description = document.selectFirst("div#syn-target")?.text() |  | ||||||
|         genre = document.select("a.label[rel=tag]").joinToString { it.text() } |  | ||||||
|         status = when (document.selectFirst("div.y6x11p i.fas.fa-rss + span.dt")?.text()) { |  | ||||||
|             "Đang tiến hành" -> SManga.ONGOING |  | ||||||
|             "Hoàn thành" -> SManga.COMPLETED |  | ||||||
|             "Tạm ngưng" -> SManga.ON_HIATUS |  | ||||||
|             "Đã huỷ" -> SManga.CANCELLED |  | ||||||
|             else -> SManga.UNKNOWN |  | ||||||
|         } |  | ||||||
|         thumbnail_url = document.selectFirst("figure img")?.attr("abs:src") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun chapterListSelector() = "li.chapter" |  | ||||||
| 
 |  | ||||||
|     override fun chapterFromElement(element: Element) = SChapter.create().apply { |  | ||||||
|         val anchor = element.selectFirst("a")!! |  | ||||||
| 
 |  | ||||||
|         setUrlWithoutDomain(anchor.attr("abs:href")) |  | ||||||
|         name = anchor.text() |  | ||||||
|         date_upload = element |  | ||||||
|             .selectFirst("time") |  | ||||||
|             ?.attr("datetime") |  | ||||||
|             ?.toLongOrNull() |  | ||||||
|             ?.times(1000L) ?: 0L |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private val mangaIdRegex = Regex("""const MANGA_ID = (\d+);""") |  | ||||||
|     private val chapterIdRegex = Regex("""const CHAPTER_ID = (\d+);""") |  | ||||||
| 
 |  | ||||||
|     @Serializable |  | ||||||
|     data class PageAjaxResponse( |  | ||||||
|         val status: Boolean = false, |  | ||||||
|         val msg: String? = null, |  | ||||||
|         val html: String, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     override fun pageListRequest(chapter: SChapter): Request { |  | ||||||
|         val html = client.newCall(GET("$baseUrl${chapter.url}")).execute().body.string() |  | ||||||
|         val chapterId = chapterIdRegex.find(html)?.groupValues?.get(1) |  | ||||||
|             ?: throw Exception("Không tìm thấy ID của chương truyện.") |  | ||||||
|         val mangaId = mangaIdRegex.find(html)?.groupValues?.get(1) |  | ||||||
| 
 |  | ||||||
|         if (mangaId != null) { |  | ||||||
|             countViews(mangaId, chapterId) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return POST("https://manga.io.vn/ajax/image/list/chap/$chapterId", headers) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun pageListParse(response: Response): List<Page> { |  | ||||||
|         val data = json.decodeFromString<PageAjaxResponse>(response.body.string()) |  | ||||||
| 
 |  | ||||||
|         if (!data.status) { |  | ||||||
|             throw Exception(data.msg) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return pageListParse(Jsoup.parse(data.html)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun pageListParse(document: Document) = |  | ||||||
|         document.select("a.readImg img").mapIndexed { i, it -> |  | ||||||
|             Page(i, imageUrl = it.attr("abs:src")) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() |  | ||||||
| 
 |  | ||||||
|     private fun countViews(mangaId: String, chapterId: String) { |  | ||||||
|         val body = FormBody.Builder() |  | ||||||
|             .add("manga", mangaId) |  | ||||||
|             .add("chapter", chapterId) |  | ||||||
|             .build() |  | ||||||
|         val request = POST( |  | ||||||
|             "$baseUrl/ajax/manga/view", |  | ||||||
|             headers, |  | ||||||
|             body, |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         runCatching { client.newCall(request).execute().close() } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     override fun getFilterList() = FilterList( |  | ||||||
|         Filter.Header("Không dùng chung với tìm kiếm bằng tên"), |  | ||||||
|         ChapterCountFilter(), |  | ||||||
|         StatusFilter(), |  | ||||||
|         GenderFilter(), |  | ||||||
|         OrderByFilter(), |  | ||||||
|         GenreList(getGenresList()), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     interface UriFilter { |  | ||||||
|         fun addToUri(builder: HttpUrl.Builder) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     open class UriPartFilter( |  | ||||||
|         name: String, |  | ||||||
|         private val query: String, |  | ||||||
|         private val vals: Array<Pair<String, String>>, |  | ||||||
|         state: Int = 0, |  | ||||||
|     ) : UriFilter, Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) { |  | ||||||
|         override fun addToUri(builder: HttpUrl.Builder) { |  | ||||||
|             builder.addQueryParameter(query, vals[state].second) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     class ChapterCountFilter : UriPartFilter( |  | ||||||
|         "Số chương", |  | ||||||
|         "chapter_count", |  | ||||||
|         arrayOf( |  | ||||||
|             ">= 0" to "0", |  | ||||||
|             ">= 10" to "10", |  | ||||||
|             ">= 30" to "30", |  | ||||||
|             ">= 50" to "50", |  | ||||||
|             ">= 100" to "100", |  | ||||||
|             ">= 200" to "200", |  | ||||||
|             ">= 300" to "300", |  | ||||||
|             ">= 400" to "400", |  | ||||||
|             ">= 500" to "500", |  | ||||||
|         ), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     class GenderFilter : UriPartFilter( |  | ||||||
|         "Giới tính", |  | ||||||
|         "sex", |  | ||||||
|         arrayOf( |  | ||||||
|             "Tất cả" to "All", |  | ||||||
|             "Con trai" to "Boy", |  | ||||||
|             "Con gái" to "Girl", |  | ||||||
|         ), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     class StatusFilter : UriPartFilter( |  | ||||||
|         "Trạng thái", |  | ||||||
|         "status", |  | ||||||
|         arrayOf( |  | ||||||
|             "Tất cả" to "", |  | ||||||
|             "Hoàn thành" to "completed", |  | ||||||
|             "Đang tiến hành" to "on-going", |  | ||||||
|             "Tạm ngưng" to "on-hold", |  | ||||||
|             "Đã huỷ" to "canceled", |  | ||||||
|         ), |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     class OrderByFilter : UriPartFilter( |  | ||||||
|         "Sắp xếp", |  | ||||||
|         "sort", |  | ||||||
|         arrayOf( |  | ||||||
|             "Mặc định" to "default", |  | ||||||
|             "Mới cập nhật" to "latest-updated", |  | ||||||
|             "Xem nhiều" to "views", |  | ||||||
|             "Xem nhiều nhất tháng" to "views_month", |  | ||||||
|             "Xem nhiều nhất tuần" to "views_week", |  | ||||||
|             "Xem nhiều nhất hôm nay" to "views_day", |  | ||||||
|             "Đánh giá cao" to "score", |  | ||||||
|             "Từ A-Z" to "az", |  | ||||||
|             "Từ Z-A" to "za", |  | ||||||
|             "Số chương nhiều nhất" to "chapters", |  | ||||||
|             "Mới nhất" to "new", |  | ||||||
|             "Cũ nhất" to "old", |  | ||||||
|         ), |  | ||||||
|         5, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     class Genre(name: String, val id: String) : Filter.TriState(name) |  | ||||||
| 
 |  | ||||||
|     class GenreList(state: List<Genre>) : UriFilter, Filter.Group<Genre>("Thể loại", state) { |  | ||||||
|         override fun addToUri(builder: HttpUrl.Builder) { |  | ||||||
|             val genres = mutableListOf<String>() |  | ||||||
|             val genresEx = mutableListOf<String>() |  | ||||||
| 
 |  | ||||||
|             state.forEach { |  | ||||||
|                 when (it.state) { |  | ||||||
|                     TriState.STATE_INCLUDE -> genres.add(it.id) |  | ||||||
|                     TriState.STATE_EXCLUDE -> genresEx.add(it.id) |  | ||||||
|                     else -> {} |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (genres.size > 0) { |  | ||||||
|                 builder.addQueryParameter("genres", genres.joinToString(",")) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (genresEx.size > 0) { |  | ||||||
|                 builder.addQueryParameter("notGenres", genresEx.joinToString(",")) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /* |  | ||||||
|         Get the list by navigating to https://manga.io.vn/filter/1 and paste in the code below |  | ||||||
|         ``` |  | ||||||
|         copy([...document.querySelectorAll("div.advanced-genres div.advance-item")].map((e) => { |  | ||||||
|             const genreId = e.querySelector("span").dataset.genre; |  | ||||||
|             const genreName = e.querySelector("label").textContent; |  | ||||||
|             return `Genre("${genreName}", "${genreId}"),` |  | ||||||
|         }).join("\n")) |  | ||||||
|         ``` |  | ||||||
|      */ |  | ||||||
|     private fun getGenresList() = listOf( |  | ||||||
|         Genre("16+", "788"), |  | ||||||
|         Genre("Action", "129"), |  | ||||||
|         Genre("Adult", "837"), |  | ||||||
|         Genre("Adventure", "810"), |  | ||||||
|         Genre("Bi Kịch", "393"), |  | ||||||
|         Genre("Cải Biên Tiểu Thuyết", "771"), |  | ||||||
|         Genre("Chuyển sinh", "287"), |  | ||||||
|         Genre("Chuyển Thể", "803"), |  | ||||||
|         Genre("Cổ Đại", "809"), |  | ||||||
|         Genre("Cổ Trang", "340"), |  | ||||||
|         Genre("Comedy", "131"), |  | ||||||
|         Genre("Comic", "828"), |  | ||||||
|         Genre("Cooking", "834"), |  | ||||||
|         Genre("Doujinshi", "201"), |  | ||||||
|         Genre("Drama", "149"), |  | ||||||
|         Genre("Ecchi", "300"), |  | ||||||
|         Genre("Fantasy", "132"), |  | ||||||
|         Genre("Full màu", "189"), |  | ||||||
|         Genre("Game", "38"), |  | ||||||
|         Genre("Gender Bender", "133"), |  | ||||||
|         Genre("gender_bender", "832"), |  | ||||||
|         Genre("Girls Love", "815"), |  | ||||||
|         Genre("Hài Hước", "791"), |  | ||||||
|         Genre("Hào Môn", "779"), |  | ||||||
|         Genre("Harem", "187"), |  | ||||||
|         Genre("Hiện đại", "285"), |  | ||||||
|         Genre("Historical", "836"), |  | ||||||
|         Genre("Hoạt Hình", "497"), |  | ||||||
|         Genre("Horror", "191"), |  | ||||||
|         Genre("Huyền Huyễn", "475"), |  | ||||||
|         Genre("Isekai", "811"), |  | ||||||
|         Genre("Josei", "395"), |  | ||||||
|         Genre("Lịch Sử", "561"), |  | ||||||
|         Genre("Ma Mị", "764"), |  | ||||||
|         Genre("Magic", "160"), |  | ||||||
|         Genre("Main Mạnh", "763"), |  | ||||||
|         Genre("Manga", "151"), |  | ||||||
|         Genre("Manh Bảo", "807"), |  | ||||||
|         Genre("Mạnh Mẽ", "818"), |  | ||||||
|         Genre("Manhua", "153"), |  | ||||||
|         Genre("Manhwa", "193"), |  | ||||||
|         Genre("Martial Arts", "614"), |  | ||||||
|         Genre("Mystery", "155"), |  | ||||||
|         Genre("Ngôn Tình", "156"), |  | ||||||
|         Genre("Ngọt Sủng", "799"), |  | ||||||
|         Genre("Nữ Cường", "819"), |  | ||||||
|         Genre("Oneshot", "65"), |  | ||||||
|         Genre("Phép Thuật", "808"), |  | ||||||
|         Genre("Phiêu Lưu", "478"), |  | ||||||
|         Genre("Psychological", "180"), |  | ||||||
|         Genre("Quái Vật", "758"), |  | ||||||
|         Genre("Romance", "756"), |  | ||||||
|         Genre("School Life", "31"), |  | ||||||
|         Genre("school_life", "833"), |  | ||||||
|         Genre("Sci-Fi", "812"), |  | ||||||
|         Genre("Seinen", "172"), |  | ||||||
|         Genre("Shoujo", "68"), |  | ||||||
|         Genre("Shoujo Ai", "136"), |  | ||||||
|         Genre("Shounen", "140"), |  | ||||||
|         Genre("Shounen Ai", "203"), |  | ||||||
|         Genre("Showbiz", "436"), |  | ||||||
|         Genre("siêu nhiên", "765"), |  | ||||||
|         Genre("Slice Of Life", "8"), |  | ||||||
|         Genre("Sports", "167"), |  | ||||||
|         Genre("Sư Tôn", "794"), |  | ||||||
|         Genre("Sủng", "820"), |  | ||||||
|         Genre("Sủng Nịch", "806"), |  | ||||||
|         Genre("Supernatural", "150"), |  | ||||||
|         Genre("Tận Thế", "759"), |  | ||||||
|         Genre("Thú Thê", "800"), |  | ||||||
|         Genre("Tiên Hiệp", "773"), |  | ||||||
|         Genre("Tình cảm", "814"), |  | ||||||
|         Genre("Tragedy", "822"), |  | ||||||
|         Genre("Tranh Sủng", "805"), |  | ||||||
|         Genre("Trap (Crossdressing)", "147"), |  | ||||||
|         Genre("Trinh Thám", "336"), |  | ||||||
|         Genre("Trọng Sinh", "398"), |  | ||||||
|         Genre("Trùng Sinh", "392"), |  | ||||||
|         Genre("Truy Thê", "780"), |  | ||||||
|         Genre("Truyện Màu", "154"), |  | ||||||
|         Genre("Truyện Nam", "761"), |  | ||||||
|         Genre("Truyện Nữ", "776"), |  | ||||||
|         Genre("Tu Tiên", "477"), |  | ||||||
|         Genre("Viễn Tưởng", "438"), |  | ||||||
|         Genre("VNComic", "787"), |  | ||||||
|         Genre("Vườn Trường", "813"), |  | ||||||
|         Genre("Webtoon", "198"), |  | ||||||
|         Genre("Xuyên Không", "157"), |  | ||||||
|         Genre("Yaoi", "593"), |  | ||||||
|         Genre("Yuri", "137"), |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
|  | |||||||
 Secozzi
						Secozzi