diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt
index a15d45cdc..347f86227 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/fmreader/FMReaderGenerator.kt
@@ -15,7 +15,6 @@ class FMReaderGenerator : ThemeSourceGenerator {
SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"),
SingleLang("KissLove", "https://klz9.com", "ja", isNsfw = true, overrideVersionCode = 4),
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR", overrideVersionCode = 3),
- SingleLang("ManhuaRock", "https://manhuarock.net", "vi", overrideVersionCode = 1),
SingleLang("Say Truyen", "https://saytruyenvip.com", "vi", overrideVersionCode = 3),
SingleLang("WeLoveManga", "https://weloma.art", "ja", pkgName = "rawlh", isNsfw = true, overrideVersionCode = 5),
SingleLang("Manga1000", "https://manga1000.top", "ja"),
diff --git a/src/vi/manhuarock/AndroidManifest.xml b/src/vi/manhuarock/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/src/vi/manhuarock/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/vi/manhuarock/build.gradle b/src/vi/manhuarock/build.gradle
new file mode 100644
index 000000000..d404f4554
--- /dev/null
+++ b/src/vi/manhuarock/build.gradle
@@ -0,0 +1,7 @@
+ext {
+ extName = "ManhuaRock"
+ extClass = ".ManhuaRock"
+ extVersionCode = 10
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/vi/manhuarock/res/mipmap-hdpi/ic_launcher.png b/src/vi/manhuarock/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..902faf434
Binary files /dev/null and b/src/vi/manhuarock/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/vi/manhuarock/res/mipmap-mdpi/ic_launcher.png b/src/vi/manhuarock/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..03d31f5e4
Binary files /dev/null and b/src/vi/manhuarock/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/vi/manhuarock/res/mipmap-xhdpi/ic_launcher.png b/src/vi/manhuarock/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..a5b3eb677
Binary files /dev/null and b/src/vi/manhuarock/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/vi/manhuarock/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/manhuarock/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d57d63ca1
Binary files /dev/null and b/src/vi/manhuarock/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/vi/manhuarock/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/manhuarock/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..208aeca45
Binary files /dev/null and b/src/vi/manhuarock/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/vi/manhuarock/src/eu/kanade/tachiyomi/extension/vi/manhuarock/ManhuaRock.kt b/src/vi/manhuarock/src/eu/kanade/tachiyomi/extension/vi/manhuarock/ManhuaRock.kt
new file mode 100644
index 000000000..2de8b551d
--- /dev/null
+++ b/src/vi/manhuarock/src/eu/kanade/tachiyomi/extension/vi/manhuarock/ManhuaRock.kt
@@ -0,0 +1,249 @@
+package eu.kanade.tachiyomi.extension.vi.manhuarock
+
+import android.util.Log
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+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 kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Single
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+class ManhuaRock : ParsedHttpSource() {
+
+ // Site changed from FMReader to some Madara copycat
+ override val versionId = 2
+
+ override val name = "ManhuaRock"
+
+ override val lang = "vi"
+
+ override val baseUrl = "https://manhuarockz.com"
+
+ override val supportsLatest = true
+
+ override fun headersBuilder() = super.headersBuilder()
+ .add("Referer", "$baseUrl/")
+
+ private val json: Json by injectLazy()
+
+ private val dateFormat by lazy {
+ SimpleDateFormat("dd MMM yyyy", Locale.US).apply {
+ timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
+ }
+ }
+
+ override fun popularMangaRequest(page: Int) = GET("$baseUrl/tat-ca-truyen/$page/?sort=most-viewd")
+
+ override fun popularMangaSelector() = "div.listupd div.page-item"
+
+ override fun popularMangaFromElement(element: Element) = SManga.create().apply {
+ val a = element.selectFirst("a")!!
+
+ setUrlWithoutDomain(a.attr("abs:href"))
+ title = a.attr("title")
+ thumbnail_url = element.selectFirst("img")?.attr("abs:data-src")
+ }
+
+ override fun popularMangaNextPageSelector() = "li.next:not(.disabled)"
+
+ override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/tat-ca-truyen/$page/?sort=latest-updated")
+
+ override fun latestUpdatesSelector() = popularMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = baseUrl.toHttpUrl().newBuilder().apply {
+ if (query.isNotBlank()) {
+ addPathSegment("search")
+ addPathSegment(page.toString())
+ addQueryParameter("keyword", query)
+ } else {
+ (if (filters.isEmpty()) getFilterList() else filters).forEach {
+ when (it) {
+ is OrderByFilter -> addQueryParameter("sort", it.values[it.state].slug)
+ is GenreList -> addPathSegments(it.values[it.state].slug)
+ else -> {}
+ }
+ }
+ addPathSegment(page.toString())
+ }
+ }.build()
+
+ return GET(url, headers)
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector()
+
+ override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ title = document.selectFirst("div.post-title h1")!!.text()
+ author = document.selectFirst("div.author-content")?.text()
+ artist = document.selectFirst("div.artist-content")?.text()
+ description = document.selectFirst("div.dsct")?.text()
+ genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
+ status = when (document.selectFirst("div.summary-heading:contains(Tình Trạng) + div.summary-content")?.text()) {
+ // I have zero idea what the strings for Ongoing and Completed are, these are educated guesses
+ // All the metadata on this page is basically "Unknown".
+ "Đang Ra" -> SManga.ONGOING
+ "Hoàn Thành" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ thumbnail_url = document.selectFirst("div.summary_image img")?.attr("abs:data-src")
+ }
+
+ override fun chapterListSelector() = "ul.row-content-chapter li"
+
+ override fun chapterFromElement(element: Element) = SChapter.create().apply {
+ val a = element.selectFirst("a")!!
+
+ setUrlWithoutDomain(a.attr("abs:href"))
+ name = a.text()
+ date_upload = runCatching {
+ val date = element.selectFirst("span.chapter-time")!!.text()
+
+ dateFormat.parse(date)!!.time
+ }.getOrDefault(0L)
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val chapterId = chapter.url.split('/').last()
+
+ return GET("$baseUrl/ajax/image/list/chap/$chapterId?mode=vertical&quality=high")
+ }
+
+ override fun pageListParse(response: Response): List {
+ val chapterId = response.request.url.pathSegments.last()
+
+ countViews(chapterId)
+
+ val data = json.decodeFromString(response.body.string())
+
+ if (!data.status || data.html == null) {
+ throw Exception(data.msg ?: "Lỗi không xác định khi lấy trang")
+ }
+
+ return pageListParse(Jsoup.parse(data.html, baseUrl))
+ }
+
+ override fun pageListParse(document: Document): List {
+ return document.select("img").mapIndexed { i, it ->
+ Page(i, imageUrl = it.attr("abs:data-src"))
+ }
+ }
+
+ override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
+
+ override fun getFilterList() = FilterList(
+ Filter.Header("Không dùng chung với tìm kiếm bằng từ khoá."),
+ OrderByFilter(),
+ GenreList(getGenreList()),
+ )
+
+ private fun countViews(chapterId: String) {
+ val req = POST("$baseUrl/ajax/manga/count-view/$chapterId")
+
+ Single.fromCallable {
+ client.newCall(req).execute().close()
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.io())
+ .subscribe(
+ {},
+ {
+ Log.e("manhuarock", "Could not count chapter view: ${it.stackTraceToString()}")
+ },
+ )
+ }
+
+ @Serializable
+ private data class AjaxImageListResponse(
+ val status: Boolean = false,
+ val msg: String? = null,
+ val html: String? = null,
+ )
+
+ private class Slug(val name: String, val slug: String) {
+ override fun toString() = name
+ }
+
+ private class OrderByFilter : Filter.Select(
+ "Sắp xếp theo",
+ arrayOf(
+ Slug("Mới cập nhật", "latest-updated"),
+ Slug("Điểm", "score"),
+ Slug("Tên A-Z", "name-az"),
+ Slug("Ngày phát hành", "release-date"),
+ Slug("Xem nhiều", "most-viewd"),
+ ),
+ 4,
+ )
+
+ private class GenreList(slugs: Array) : Filter.Select("Thể loại", slugs)
+
+ private fun getGenreList() = arrayOf(
+ Slug("Tất cả", "tat-ca-truyen"),
+ Slug("Hoàn thành", "hoan-thanh"),
+ Slug("Xuyên Không", "the-loai/xuyen-khong"),
+ Slug("Webtoon", "the-loai/webtoon"),
+ Slug("Truyện Màu", "the-loai/truyen-mau"),
+ Slug("Trọng Sinh", "the-loai/trong-sinh"),
+ Slug("Tragedy", "the-loai/tragedy"),
+ Slug("Supernatural", "the-loai/supernatural"),
+ Slug("Sports", "the-loai/sports"),
+ Slug("Slice Of Life", "the-loai/slice-of-life"),
+ Slug("Shounen", "the-loai/shounen"),
+ Slug("Shoujo", "the-loai/shoujo"),
+ Slug("Sci-Fi", "the-loai/sci-fi"),
+ Slug("School Life", "the-loai/school-life"),
+ Slug("Romance", "the-loai/romance"),
+ Slug("Psychological", "the-loai/psychological"),
+ Slug("Ngôn Tình", "the-loai/ngon-tinh"),
+ Slug("Mystery", "the-loai/mystery"),
+ Slug("Mature", "the-loai/mature"),
+ Slug("Martial Arts", "the-loai/martial-arts"),
+ Slug("Manhwa", "the-loai/manhwa"),
+ Slug("Manhua", "the-loai/manhua"),
+ Slug("Josei", "the-loai/josei"),
+ Slug("Isekai", "the-loai/isekai"),
+ Slug("Huyền Huyễn", "the-loai/huyen-huyen"),
+ Slug("Horror", "the-loai/horror"),
+ Slug("Historical", "the-loai/historical"),
+ Slug("Harem", "the-loai/harem"),
+ Slug("Gender Bender", "the-loai/gender-bender"),
+ Slug("Fantasy", "the-loai/fantasy"),
+ Slug("Ecchi", "the-loai/ecchi"),
+ Slug("Drama", "the-loai/drama"),
+ Slug("Detective", "the-loai/detective"),
+ Slug("Demons", "the-loai/demons"),
+ Slug("Comedy", "the-loai/comedy"),
+ Slug("Cổ Đại", "the-loai/co-dai"),
+ Slug("Chuyển Sinh", "the-loai/chuyen-sinh"),
+ Slug("Anime", "the-loai/anime"),
+ Slug("Adventure", "the-loai/adventure"),
+ Slug("Adult", "the-loai/adult"),
+ Slug("Action", "the-loai/action"),
+ )
+}