diff --git a/src/en/manhuaplusorg/AndroidManifest.xml b/src/en/manhuaplusorg/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/en/manhuaplusorg/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/manhuaplusorg/build.gradle b/src/en/manhuaplusorg/build.gradle
new file mode 100644
index 000000000..b996031e7
--- /dev/null
+++ b/src/en/manhuaplusorg/build.gradle
@@ -0,0 +1,7 @@
+ext {
+ extName = 'ManhuaPlus (unoriginal)'
+ extClass = '.ManhuaPlusOrg'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/manhuaplusorg/res/mipmap-hdpi/ic_launcher.png b/src/en/manhuaplusorg/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..7fe8797e0
Binary files /dev/null and b/src/en/manhuaplusorg/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/manhuaplusorg/res/mipmap-mdpi/ic_launcher.png b/src/en/manhuaplusorg/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..757b999ef
Binary files /dev/null and b/src/en/manhuaplusorg/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/manhuaplusorg/res/mipmap-xhdpi/ic_launcher.png b/src/en/manhuaplusorg/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..88da4a8b3
Binary files /dev/null and b/src/en/manhuaplusorg/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/manhuaplusorg/res/mipmap-xxhdpi/ic_launcher.png b/src/en/manhuaplusorg/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4bab447a2
Binary files /dev/null and b/src/en/manhuaplusorg/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/manhuaplusorg/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/manhuaplusorg/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..59d18aacd
Binary files /dev/null and b/src/en/manhuaplusorg/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt
new file mode 100644
index 000000000..5751e5cae
--- /dev/null
+++ b/src/en/manhuaplusorg/src/eu/kanade/tachiyomi/extension/en/manhuaplusorg/ManhuaPlusOrg.kt
@@ -0,0 +1,242 @@
+package eu.kanade.tachiyomi.extension.en.manhuaplusorg
+
+import eu.kanade.tachiyomi.network.GET
+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() {
+
+ override val name = "ManhuaPlus (Unoriginal)"
+
+ 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 {
+ 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"),
+ )
+ }
+}