diff --git a/src/en/mangasect/AndroidManifest.xml b/src/en/mangasect/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/en/mangasect/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/mangasect/build.gradle b/src/en/mangasect/build.gradle
new file mode 100644
index 000000000..86f4f6844
--- /dev/null
+++ b/src/en/mangasect/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Manga Sect'
+ pkgNameSuffix = 'en.mangasect'
+ extClass = '.MangaSect'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/mangasect/res/mipmap-hdpi/ic_launcher.png b/src/en/mangasect/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..9989b486f
Binary files /dev/null and b/src/en/mangasect/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/mangasect/res/mipmap-mdpi/ic_launcher.png b/src/en/mangasect/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..3b2068f01
Binary files /dev/null and b/src/en/mangasect/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/mangasect/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangasect/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..1d8703e0c
Binary files /dev/null and b/src/en/mangasect/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/mangasect/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangasect/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d3cb384ad
Binary files /dev/null and b/src/en/mangasect/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/mangasect/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangasect/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4595b48fc
Binary files /dev/null and b/src/en/mangasect/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/mangasect/res/web_hi_res_512.png b/src/en/mangasect/res/web_hi_res_512.png
new file mode 100644
index 000000000..99671ccea
Binary files /dev/null and b/src/en/mangasect/res/web_hi_res_512.png differ
diff --git a/src/en/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSect.kt b/src/en/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSect.kt
new file mode 100644
index 000000000..c58c9be02
--- /dev/null
+++ b/src/en/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSect.kt
@@ -0,0 +1,238 @@
+package eu.kanade.tachiyomi.extension.en.mangasect
+
+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 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 MangaSect : ParsedHttpSource() {
+
+ override val name = "Manga Sect"
+
+ override val baseUrl = "https://mangasect.com"
+
+ 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("Not used")
+
+ override fun latestUpdatesFromElement(element: Element): SManga =
+ throw UnsupportedOperationException("Not used")
+
+ override fun latestUpdatesNextPageSelector(): String =
+ throw UnsupportedOperationException("Not used")
+
+ // 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.toString(), headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
+
+ override fun searchMangaSelector(): String =
+ throw UnsupportedOperationException("Not used")
+
+ override fun searchMangaFromElement(element: Element): SManga =
+ throw UnsupportedOperationException("Not used")
+
+ override fun searchMangaNextPageSelector(): String =
+ throw UnsupportedOperationException("Not used")
+
+ // 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"))
+ }
+ }
+
+ // Pages
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ 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()
+
+ val id = chapter.url.split("/").last()
+ 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.attr("data-index").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/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSectFilters.kt b/src/en/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSectFilters.kt
new file mode 100644
index 000000000..92fbaa4fc
--- /dev/null
+++ b/src/en/mangasect/src/eu/kanade/tachiyomi/extension/en/mangasect/MangaSectFilters.kt
@@ -0,0 +1,167 @@
+package eu.kanade.tachiyomi.extension.en.mangasect
+
+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", "most-viewd"),
+ 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", "29"),
+ Pair("Adaptation", "66"),
+ Pair("Adult", "108"),
+ Pair("Adventure", "33"),
+ Pair("Aliens", "2326"),
+ Pair("Animals", "199"),
+ Pair("Comedy", "35"),
+ Pair("Comic", "109"),
+ Pair("Cooking", "26"),
+ Pair("Crime", "274"),
+ Pair("Delinquents", "234"),
+ Pair("Demons", "136"),
+ Pair("Drama", "39"),
+ Pair("Dungeons", "204"),
+ Pair("Ecchi", "54"),
+ Pair("Fantasy", "30"),
+ Pair("Full Color", "27"),
+ Pair("Genderswap", "1441"),
+ Pair("Genius MC", "209"),
+ Pair("Ghosts", "1527"),
+ Pair("Gore", "1678"),
+ Pair("Harem", "43"),
+ Pair("Historical", "49"),
+ Pair("Horror", "69"),
+ Pair("Incest", "1189"),
+ Pair("Isekai", "40"),
+ Pair("Loli", "198"),
+ Pair("Long Strip", "233"),
+ Pair("Magic", "212"),
+ Pair("Magical Girls", "1676"),
+ Pair("Manhua", "58"),
+ Pair("Manhwa", "80"),
+ Pair("Martial Arts", "32"),
+ Pair("Mature", "34"),
+ Pair("Mecha", "70"),
+ Pair("Medical", "2113"),
+ Pair("Military", "1531"),
+ Pair("Monster", "218"),
+ Pair("Monster Girls", "201"),
+ Pair("Monsters", "63"),
+ Pair("Murim", "208"),
+ Pair("Music", "412"),
+ Pair("Mystery", "31"),
+ Pair("One shot", "155"),
+ Pair("Overpowered", "206"),
+ Pair("Police", "275"),
+ Pair("Post-Apocalyptic", "197"),
+ Pair("Psychological", "36"),
+ Pair("Rebirth", "1435"),
+ Pair("Recarnation", "67"),
+ Pair("Regression", "205"),
+ Pair("Reincarnation", "64"),
+ Pair("Return", "1454"),
+ Pair("Returner", "211"),
+ Pair("Revenge", "219"),
+ Pair("Romance", "37"),
+ Pair("School Life", "44"),
+ Pair("Sci fi", "42"),
+ Pair("Sci-fi", "216"),
+ Pair("Seinen", "52"),
+ Pair("Sexual Violence", "2325"),
+ Pair("Shota", "2327"),
+ Pair("Shoujo", "92"),
+ Pair("Shounen", "38"),
+ Pair("Shounen ai", "103"),
+ Pair("Slice of Life", "68"),
+ Pair("Super power", "213"),
+ Pair("Superhero", "1630"),
+ Pair("Supernatural", "41"),
+ Pair("Survival", "463"),
+ Pair("System", "203"),
+ Pair("Thriller", "462"),
+ Pair("Time travel", "65"),
+ Pair("tower", "207"),
+ Pair("Tragedy", "51"),
+ Pair("Transmigration", "217"),
+ Pair("Uncategorized", "55"),
+ Pair("Vampires", "200"),
+ Pair("Video Games", "1606"),
+ Pair("Virtual Reality", "757"),
+ Pair("Web comic", "98"),
+ Pair("Webtoons", "77"),
+ Pair("Wuxia", "202"),
+ Pair("Zombies", "464"),
+ )
+ }
+}