diff --git a/src/vi/truyenhentai18/AndroidManifest.xml b/src/vi/truyenhentai18/AndroidManifest.xml
new file mode 100644
index 000000000..d432c95fb
--- /dev/null
+++ b/src/vi/truyenhentai18/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/vi/truyenhentai18/build.gradle b/src/vi/truyenhentai18/build.gradle
new file mode 100644
index 000000000..82d8540f1
--- /dev/null
+++ b/src/vi/truyenhentai18/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = "Truyen Hentai 18+"
+ extClass = ".TruyenHentai18"
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/vi/truyenhentai18/res/mipmap-hdpi/ic_launcher.png b/src/vi/truyenhentai18/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..f2c7bd873
Binary files /dev/null and b/src/vi/truyenhentai18/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/vi/truyenhentai18/res/mipmap-mdpi/ic_launcher.png b/src/vi/truyenhentai18/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..182d4a5d9
Binary files /dev/null and b/src/vi/truyenhentai18/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/vi/truyenhentai18/res/mipmap-xhdpi/ic_launcher.png b/src/vi/truyenhentai18/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..b67ea6f7d
Binary files /dev/null and b/src/vi/truyenhentai18/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/vi/truyenhentai18/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/truyenhentai18/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..249c9896a
Binary files /dev/null and b/src/vi/truyenhentai18/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/vi/truyenhentai18/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/truyenhentai18/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d4d2169a3
Binary files /dev/null and b/src/vi/truyenhentai18/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18.kt b/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18.kt
new file mode 100644
index 000000000..08f6aff67
--- /dev/null
+++ b/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18.kt
@@ -0,0 +1,150 @@
+package eu.kanade.tachiyomi.extension.vi.truyenhentai18
+
+import eu.kanade.tachiyomi.network.GET
+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 okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import java.util.Calendar
+
+class TruyenHentai18 : ParsedHttpSource() {
+
+ override val name = "Truyện Hentai 18+"
+
+ override val baseUrl = "https://truyenhentai18.net"
+
+ override val lang = "vi"
+
+ override val supportsLatest = true
+
+ override val client = network.cloudflareClient
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ override fun popularMangaRequest(page: Int) =
+ GET("$baseUrl/truyen-de-xuat" + if (page > 1) "/page/$page" else "", headers)
+
+ override fun popularMangaSelector() = "div.row > div[class^=item-] > div.card"
+
+ override fun popularMangaFromElement(element: Element) = SManga.create().apply {
+ element.selectFirst("a.item-title")!!.let {
+ setUrlWithoutDomain(it.attr("href"))
+ title = it.text()
+ }
+
+ thumbnail_url = element.selectFirst("a.item-cover img")?.absUrl("data-src")
+ }
+
+ override fun popularMangaNextPageSelector() = "ul.pagination li.page-item.active:not(:last-child)"
+
+ override fun latestUpdatesRequest(page: Int) =
+ GET("$baseUrl/truyen-moi" + if (page > 1) "/page/$page" else "", headers)
+
+ override fun latestUpdatesSelector() = popularMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun fetchSearchManga(
+ page: Int,
+ query: String,
+ filters: FilterList,
+ ): Observable {
+ return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
+ val slug = query.removePrefix(PREFIX_SLUG_SEARCH)
+ val url = "/$slug"
+
+ fetchMangaDetails(SManga.create().apply { this.url = url })
+ .map { MangasPage(listOf(it.apply { this.url = url }), false) }
+ } else {
+ super.fetchSearchManga(page, query, filters)
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = baseUrl.toHttpUrl().newBuilder().apply {
+ if (page > 1) {
+ addPathSegment("page")
+ addPathSegment(page.toString())
+ }
+
+ addQueryParameter("s", query)
+ }.build()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaSelector() = "div[data-id] > div.card"
+
+ override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ val statusClassName = document.selectFirst("em.eflag.item-flag")!!.className()
+
+ title = document.selectFirst("span[itemprop=name]")!!.text()
+ author = document.select("div.attr-item b:contains(Tác giả) ~ span a, span[itemprop=author]").joinToString { it.text() }
+ description = document.selectFirst("div[itemprop=about]")?.text()
+ genre = document.select("ul.post-categories li a").joinToString { it.text() }
+ thumbnail_url = document.selectFirst("div.attr-cover img")?.absUrl("src")
+ status = when {
+ statusClassName.contains("flag-completed") -> SManga.COMPLETED
+ statusClassName.contains("flag-ongoing") -> SManga.ONGOING
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ override fun chapterListSelector() = "#chaptersbox > div"
+
+ override fun chapterFromElement(element: Element) = SChapter.create().apply {
+ element.selectFirst("a")!!.let {
+ setUrlWithoutDomain(it.attr("href"))
+ name = it.selectFirst("b")!!.text()
+ }
+
+ date_upload = element.selectFirst("div.extra > i.ps-3")
+ ?.text()
+ ?.let { parseRelativeDate(it) }
+ ?: 0L
+ }
+
+ override fun pageListParse(document: Document) =
+ document.select("#viewer img").mapIndexed { i, it ->
+ Page(i, imageUrl = it.absUrl("src"))
+ }
+
+ override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
+
+ private fun parseRelativeDate(date: String): Long {
+ val (valueString, unit) = date.substringBefore(" trước").split(" ")
+ val value = valueString.toInt()
+
+ val calendar = Calendar.getInstance().apply {
+ when (unit) {
+ "giây" -> add(Calendar.SECOND, -value)
+ "phút" -> add(Calendar.MINUTE, -value)
+ "giờ" -> add(Calendar.HOUR_OF_DAY, -value)
+ "ngày" -> add(Calendar.DAY_OF_MONTH, -value)
+ "tuần" -> add(Calendar.WEEK_OF_MONTH, -value)
+ "tháng" -> add(Calendar.MONTH, -value)
+ "năm" -> add(Calendar.YEAR, -value)
+ }
+ }
+
+ return calendar.timeInMillis
+ }
+
+ companion object {
+ internal const val PREFIX_SLUG_SEARCH = "slug:"
+ }
+}
diff --git a/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18UrlActivity.kt b/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18UrlActivity.kt
new file mode 100644
index 000000000..91a3fe261
--- /dev/null
+++ b/src/vi/truyenhentai18/src/eu/kanade/tachiyomi/extension/vi/truyenhentai18/TruyenHentai18UrlActivity.kt
@@ -0,0 +1,35 @@
+package eu.kanade.tachiyomi.extension.vi.truyenhentai18
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+class TruyenHentai18UrlActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val pathSegments = intent?.data?.pathSegments
+
+ if (pathSegments != null && pathSegments.size > 0) {
+ val intent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${TruyenHentai18.PREFIX_SLUG_SEARCH}${pathSegments[0]}")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e("TruyenHentaiUrlActivity", "Could not start activity", e)
+ }
+ } else {
+ Log.e("TruyenHentaiUrlActivity", "Could not parse URI from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}