diff --git a/src/en/mangabtt/AndroidManifest.xml b/src/en/mangabtt/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/en/mangabtt/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/mangabtt/build.gradle b/src/en/mangabtt/build.gradle
new file mode 100644
index 000000000..8f7233eb9
--- /dev/null
+++ b/src/en/mangabtt/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'MangaBTT'
+ extClass = '.MangaBTT'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/mangabtt/res/mipmap-hdpi/ic_launcher.png b/src/en/mangabtt/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..fe9b345a3
Binary files /dev/null and b/src/en/mangabtt/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/mangabtt/res/mipmap-mdpi/ic_launcher.png b/src/en/mangabtt/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..70fc06d95
Binary files /dev/null and b/src/en/mangabtt/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/mangabtt/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangabtt/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..defd761e3
Binary files /dev/null and b/src/en/mangabtt/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/mangabtt/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangabtt/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..64e0b45e2
Binary files /dev/null and b/src/en/mangabtt/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/mangabtt/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangabtt/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..72b23c5f9
Binary files /dev/null and b/src/en/mangabtt/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTT.kt b/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTT.kt
new file mode 100644
index 000000000..cf3ec31b1
--- /dev/null
+++ b/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTT.kt
@@ -0,0 +1,263 @@
+package eu.kanade.tachiyomi.extension.en.mangabtt
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+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.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import okhttp3.FormBody
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.util.Calendar
+
+class MangaBTT : ParsedHttpSource() {
+
+ override val name = "MangaBTT"
+
+ override val baseUrl = "https://mangabtt.com"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ override val client by lazy {
+ network.cloudflareClient.newBuilder()
+ .rateLimit(2)
+ .build()
+ }
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ // ============================== Popular ===============================
+
+ override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
+ page = page,
+ query = "",
+ filters = FilterList(
+ SortByFilter(default = 2),
+ StatusFilter(default = 1),
+ GenreFilter(default = 1),
+ ),
+ )
+
+ override fun popularMangaSelector(): String =
+ searchMangaSelector()
+
+ override fun popularMangaFromElement(element: Element): SManga =
+ searchMangaFromElement(element)
+
+ override fun popularMangaNextPageSelector(): String =
+ searchMangaNextPageSelector()
+
+ // =============================== Latest ===============================
+
+ override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
+ page = page,
+ query = "",
+ filters = FilterList(
+ SortByFilter(default = 8),
+ StatusFilter(default = 1),
+ GenreFilter(default = 1),
+ ),
+ )
+
+ override fun latestUpdatesSelector(): String =
+ searchMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga =
+ searchMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector(): String =
+ searchMangaNextPageSelector()
+
+ // =============================== Search ===============================
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/find-story".toHttpUrl().newBuilder().apply {
+ if (query.isNotBlank()) {
+ addQueryParameter("keyword", query)
+ } else {
+ val genre = filters.firstInstanceOrNull()?.selectedValue.orEmpty()
+ val status = filters.firstInstanceOrNull()?.selectedValue.orEmpty()
+ val sortBy = filters.firstInstanceOrNull()?.selectedValue.orEmpty()
+
+ addQueryParameter("status", status)
+ addQueryParameter("sort", sortBy)
+ if (genre.isNotBlank()) {
+ addPathSegment(genre)
+ }
+ }
+
+ addQueryParameter("page", page.toString())
+ }
+
+ return GET(url.build(), headers)
+ }
+
+ override fun searchMangaSelector(): String = ".items > .row > .item"
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ thumbnail_url = element.selectFirst(".image img")?.imgAttr()
+ element.selectFirst("figcaption h3 a")!!.run {
+ title = text()
+ setUrlWithoutDomain(attr("abs:href"))
+ }
+ }
+
+ override fun searchMangaNextPageSelector(): String =
+ "ul.pagination > li.active + li:not(.disabled)"
+
+ // =============================== Filters ==============================
+
+ override fun getFilterList(): FilterList = FilterList(
+ Filter.Header("Ignored when using text search"),
+ Filter.Separator(),
+ GenreFilter(),
+ StatusFilter(),
+ SortByFilter(),
+ )
+
+ // =========================== Manga Details ============================
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ title = document.selectFirst("h1.title-detail")!!.text()
+ description = document.selectFirst(".detail-content p")?.text()
+ ?.substringAfter("comic site. The Summary is ")
+
+ document.selectFirst(".detail-info")?.run {
+ thumbnail_url = selectFirst("img")?.imgAttr()
+ status = selectFirst(".status p:not(.name)").parseStatus()
+ genre = select(".kind a").joinToString(", ") { it.text() }
+ author = selectFirst(".author p:not(.name)")?.text()?.takeUnless {
+ it.equals("updating", true)
+ }
+ }
+ }
+
+ private fun Element?.parseStatus(): Int = with(this?.text()) {
+ return when {
+ equals("ongoing", true) -> SManga.ONGOING
+ equals("Đang cập nhật", true) -> SManga.ONGOING
+ equals("completed", true) -> SManga.COMPLETED
+ equals("on-hold", true) -> SManga.ON_HIATUS
+ equals("canceled", true) -> SManga.CANCELLED
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ // ============================== Chapters ==============================
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val postHeaders = headersBuilder().apply {
+ add("Accept", "*/*")
+ add("Host", baseUrl.toHttpUrl().host)
+ add("Origin", baseUrl)
+ set("Referer", baseUrl + manga)
+ add("X-Requested-With", "XMLHttpRequest")
+ }.build()
+
+ val postBody = FormBody.Builder()
+ .add("StoryID", manga.url.substringAfterLast("-"))
+ .build()
+
+ return POST("$baseUrl/Story/ListChapterByStoryID", postHeaders, postBody)
+ }
+
+ override fun chapterListSelector() = "ul > li:not(.heading)"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ element.selectFirst(".col-xs-4")?.also {
+ date_upload = it.text().parseRelativeDate()
+ }
+ element.selectFirst("a")!!.run {
+ name = text()
+ setUrlWithoutDomain(attr("abs:href"))
+ }
+ }
+
+ // From OppaiStream
+ private fun String.parseRelativeDate(): Long {
+ val now = Calendar.getInstance().apply {
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }
+
+ var parsedDate = 0L
+ val relativeDate = this.split(" ").firstOrNull()
+ ?.replace("one", "1")
+ ?.replace("a", "1")
+ ?.toIntOrNull()
+ ?: return 0L
+
+ when {
+ // parse: 30 seconds ago
+ "second" in this -> {
+ parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
+ }
+ // parses: "42 minutes ago"
+ "minute" in this -> {
+ parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
+ }
+ // parses: "1 hour ago" and "2 hours ago"
+ "hour" in this -> {
+ parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
+ }
+ // parses: "2 days ago"
+ "day" in this -> {
+ parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
+ }
+ // parses: "2 weeks ago"
+ "week" in this -> {
+ parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
+ }
+ // parses: "2 months ago"
+ "month" in this -> {
+ parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
+ }
+ // parse: "2 years ago"
+ "year" in this -> {
+ parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
+ }
+ }
+ return parsedDate
+ }
+
+ // =============================== Pages ================================
+
+ override fun pageListParse(document: Document): List {
+ return document.select(".reading-detail > .page-chapter").map { page ->
+ val img = page.selectFirst("img[data-index]")!!
+ val index = img.attr("data-index").toInt()
+ val url = img.imgAttr()
+ Page(index, imageUrl = 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 ==============================
+
+ 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")
+ }
+}
diff --git a/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTTFilters.kt b/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTTFilters.kt
new file mode 100644
index 000000000..fbf85082f
--- /dev/null
+++ b/src/en/mangabtt/src/eu/kanade/tachiyomi/extension/en/mangabtt/MangaBTTFilters.kt
@@ -0,0 +1,76 @@
+package eu.kanade.tachiyomi.extension.en.mangabtt
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+data class FilterOption(val displayName: String, val value: String)
+
+inline fun List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T
+
+open class EnhancedSelect(name: String, private val _values: List, state: Int = 0) :
+ Filter.Select(name, _values.map { it.displayName }.toTypedArray(), state) {
+
+ val selectedValue: String?
+ get() = _values.getOrNull(state)?.value
+}
+
+class SortByFilter(default: Int = 1) : EnhancedSelect(
+ "Sort By",
+ listOf(
+ FilterOption("Top day", "13"),
+ FilterOption("Top week", "12"),
+ FilterOption("Top month", "11"),
+ FilterOption("Top All", "10"),
+ FilterOption("Comment", "25"),
+ FilterOption("New Manga", "15"),
+ FilterOption("Chapter", "30"),
+ FilterOption("Latest Updates", "0"),
+ ),
+ default - 1,
+)
+
+class StatusFilter(default: Int = 1) : EnhancedSelect(
+ "Status",
+ listOf(
+ FilterOption("All", "-1"),
+ FilterOption("Completed", "2"),
+ FilterOption("Ongoing", "1"),
+ ),
+ default - 1,
+)
+
+class GenreFilter(default: Int = 1) : EnhancedSelect(
+ "Genre",
+ listOf(
+ FilterOption("All", ""),
+ FilterOption("Action", "action"),
+ FilterOption("ADVENTURE", "adventure"),
+ FilterOption("Comedy", "comedy"),
+ FilterOption("Cooking", "cooking"),
+ FilterOption("Drama", "drama"),
+ FilterOption("Fantasy", "fantasy"),
+ FilterOption("Historical", "historical"),
+ FilterOption("Horror", "horror"),
+ FilterOption("Isekai", "isekai"),
+ FilterOption("Josei", "josei"),
+ FilterOption("Manhua", "manhua"),
+ FilterOption("Manhwa", "manhwa"),
+ FilterOption("Martial Arts", "martial-arts"),
+ FilterOption("Mecha", "mecha"),
+ FilterOption("MYSTERY", "mystery"),
+ FilterOption("PSYCHOLOGICAL", "psychological"),
+ FilterOption("Romance", "romance"),
+ FilterOption("School Life", "school-life"),
+ FilterOption("Sci fi", "sci-fi"),
+ FilterOption("Seinen", "seinen"),
+ FilterOption("Shoujo", "shoujo"),
+ FilterOption("Shounen", "shounen"),
+ FilterOption("SLICE OF LIF", "slice-of-lif"),
+ FilterOption("Slice of Life", "slice-of-life"),
+ FilterOption("Sports", "sports"),
+ FilterOption("SUGGESTIVE", "suggestive"),
+ FilterOption("SUPERNATURAL", "supernatural"),
+ FilterOption("TRAGEDY", "tragedy"),
+ FilterOption("Webtoons", "webtoons"),
+ ),
+ default - 1,
+)