Pow=G~8
zE%$K`10LS)qPU4nZ*sXZ&$k<(HbiweoMsxlwN$!TR_7p
z`-Gq@*aAlx_brVYG>`c9_Ot-iKn#IXq0^x*uiBv!A{r7$1{+wO}STM9Kz8w
z|1v!m40piN!G4F_F<|QVDf&9`fmO{-IbMK&zA$u)VW8OOM?D*56{;sQN3!ADRy3Ah8qlK7Qa~;!m67x_oz;OM!&Ilyikcpo9^|iM-qpMwa2=l
zo&&?<;w>;~=vCrRTnK5&?%-?PPBlH6{77p<&mHTWu(fo2Hv2?%_DIUBK*xDI?%aWqSvx%kLnz
zxLdJ!(W>pzL8p^Ae}`GekM~t$XNNj(J@Ps@LkJ9gi)8e=JeJ$Yt860NCjx1Jg(HcY
zac5NHgT6@x@APX{isKdcY!{|5o_SY*R&pZLb=nu12A4z|Zz6UHpeOC|dOZ?i-^{-}
zKkfhc^u`}^XOh1xWo~?L*2*>Zb$r~N5o6G?kpuVE^&$=W{Bbr&2tBe|!_k9ttn=br|3nM=U(l_q6iQ5*u152a#C=QpIo
zkxGX{&Q$ouC&2Tcn{yV`N2yZF3B^L
z3e>1UYFxd5u=YyB)s5{gb$7n&Do&g3;S}5ich`$~q^h2u>qCiue*PT6a$kzSwHMIj
z{nfVNjd_3SiQ>n6D&6kaOHjm3FYH0QN)g7+_g$>W^%<)8Vo>NAseK@;YlZmxC+*TE
z9q+H}Z0DZP3fI>V^mhXZ(&Pg_6GB93OP(pU>xEWcl~-^7y(iE?H8H(G+wz|C8YL})
zhWQi}zH(_q-1Hao4_>yk35D4%TC~5e-fzyNF0w(T8Wn_+ELb?Ah+W+l@CCfPAIdCc
zzFc4YPbTj@7`6YLG
zH}e<|TN_7xUnpX9Ve|mq>_A|h!AM}))+DeL$6La<=1gM8M{+MYN@C5vF6A}fjCos3_~nQ7emZ_@5z1(ew4j=
z9mx67#9m{ao$W2;*@h12iAq`Og-0sd!E?Bxx=lLCxSKlqIGLk-av#
z)#7`(R&=f_Rzju@f<@%Uq}Gem9vms`!f>Pdx5K&(5TU?itek!i9Vt3%
zq<&P*rwY#NqK-XIg-KWE<@;+TpW5WutMu
zK$P-kXIgi&=Vv%WzQ#pYMCZ$yBrhlHB&jEg6j!$3xb-fbkkKogZD<^gigwt{WR(0L
za4BaGB}Ak`L=OK3Wh~p14vLFYR8Kgt{tYy4OBE@zH1<;wvJ@oSDE)|E{Zt%Uz%m5W
z!fo~Iw}V`3w+MZaifo+p&E9rsN@`Am%`{1%^jV$47iNk?ih-ATiB3?+@L{EM3H}in
z+E@cM^1
zjxRJaBrY7dkMg1utfN0ZO1;Q$%u?k+0=m`;)mi(aP)&Ta9wDOJUYmKgC&rhS8ag24
zArG&x{bFRzzdAZySF3HZs#cIJcMY7vS9$C|ddWDD8@<(>Hh6EiSNbXd
z72KSjEl(~NR~+e~`M0?A92LRI`o}MN?M_O$Ts@1(6XY=RXCdiiF8Pd7^XQ~cYP5|V
z>;}t{Jh5!3U%p^s(DS%5Qr3}CgNK2J1R+*jloi!HIwd-Fb-;qAXjJo#9}!34N|$(7OG%#vrXL*Afqi5cZDAda(xJYV^pr%ELYb1U}iZ_Z4(9xusf%kDl)0WSViU^LZk6#-K)ld1O
z_|=C&O0$mp?d_G;%g^FEiLwDZ##R)K#pI01>y6$rva!?~Q!y|6B9Zv)8UCkV%?eyZ
zc-3288v6`#*<(=k?r5_5ZkGgU8Jgic+N<_U5WYwGxAIZtWcfl^VnJT^G7rl)kxO~V
zZiHZbRE@IP&rhs;$@reSTE&OXD#dIcSSQO$3^af`QAI5;po%
zv)#1Tw>r1-PVaue#Q4sC#{lzcDXL=Haib?Gmec9G<@prES-kALPMJ|fXUaLbit)r>
zw38-Jn*T@q`afc+`4K80Qizwdt#V#5Vquc8%+h+(>P)Araph9=K{aHSS5a?r7Pe{fCMCE3nF
zNWhO&`)fEb1a~&L;rm?c#NP)dsbR%|JP0V}>CMk;wBo#Hv8a0*Je3
zkq)gK>>M>r?^k!z>ZlT7+YgnKMz{@aSI+-8n$1|!bdUK%K#1jRG&i8)MUk;>7aH>c
z(S_6CnDqDy_CGW$)Ae;7JSK==_86Nv9$}etMgf)%+OloxP6&RlU78{fpXit#
zjt{ZU)#u{?^Z!KKyicRKtb|SX*<|e0vT}<^Ie?Nq5Kqqv0*Ptl;EDJD9h^~5fdh!t
z7|M@rL8H~d{&bbEyD{Cxmw&>5@XuW(quzInPmadWOr0FTBi6JGE;<94i3qoEGUTsk
zS*Yrq(h3*ja&i81cmxgu!3?MQE)|ajwEPvTv838w11l&}wn20S5FW5qF$S0SC@=c|
z9kVfstmJQy-tU#&4(I=*;Wo=4g;jNwH9WQ|v=HO~eYj=qp9(hp%9SkXZ;&!4^)t_rX^(TXo6|H9^|V<^+)Ttf>6U|LLD%t$0suRCUfCw1uX^m)EJJ$|AP4%yfpUi|)na+k!4{QwtHbkD0L
zYQ=;YNSN{&R~$60526G0@2BTP=nx^;Ks>Az;te4%mdSUY@7wi$O=q%ry|qI@W)AdUtetX-QN`p?w%>|{P(Jr3Xt{F6o88j`)Q32
zC?UNx(R6PFv6)T0*fZn3BrYC{RY*V6TOz{C6O<>k|Fs>G#H044$3irE
zdDggj;}dxI!4A>qe3L%cXY`dT7Lo^q-Qj_e)2WfbW}Q#aRB~zcDWy}l0m7Ta(BG8t
z;rp<$GuYLrJ))qY8f#&vbNTrXvBBrLTS6oIxe3$?wukq3Nfrvr52GIo;1^ULfNnNN
z8eI~LMk3Hrmu$m_fq|j!qadpCGoUv8Kz-S5oq-*!i4U&Nu!(j(CN!jf?d9mbG>~}w
z8x~$X;qbnqfz^dJ$bbwO5sp3IExy_-E?oC3EX%xTO#<E|EO(Y#V$KRPHGb|iP1lN+})Q3+ll&5!~F^X
zVmC(DJBSeIy43aDN{Fmvoa7-kWyxqHezD?>c)SarpBE4{V$I<37ZBjlqtFq0n&>LZ
z$I&U1tgLsy;%YVWRn3Qf_TCi
z@`FdSl;YWQW{^DwFtx_ClfoS8Y_}yl0?dPUJ1_Z@o2p+)mZtGf+_2GR&&v_~**xxg
z_J|O9WWN@(
+ 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 {
+ val data = response.parseAs().html
+ return pageListParse(
+ Jsoup.parseBodyFragment(
+ data,
+ response.request.header("Referer")!!,
+ ),
+ )
+ }
+
+ override fun pageListParse(document: Document): List {
+ 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 Response.parseAs(): T {
+ return json.decodeFromString(body.string())
+ }
+}
diff --git a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt
new file mode 100644
index 000000000..ab7e83da2
--- /dev/null
+++ b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrgFilters.kt
@@ -0,0 +1,139 @@
+package eu.kanade.tachiyomi.extension.en.manhuaplusorg
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+abstract class SelectFilter(
+ name: String,
+ private val options: List>,
+) : Filter.Select(
+ 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(
+ "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"),
+ )
+ }
+}