diff --git a/multisrc/overrides/mymangacms/default/AndroidManifest.xml b/multisrc/overrides/mymangacms/default/AndroidManifest.xml
new file mode 100644
index 000000000..8a9512979
--- /dev/null
+++ b/multisrc/overrides/mymangacms/default/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..c42b4063a
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..b2150bb89
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dc243cd2a
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..580330e4f
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1d00d3744
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/res/web_hi_res_512.png b/multisrc/overrides/mymangacms/lkdtt/res/web_hi_res_512.png
new file mode 100644
index 000000000..57120ab13
Binary files /dev/null and b/multisrc/overrides/mymangacms/lkdtt/res/web_hi_res_512.png differ
diff --git a/multisrc/overrides/mymangacms/lkdtt/src/LKDTT.kt b/multisrc/overrides/mymangacms/lkdtt/src/LKDTT.kt
new file mode 100644
index 000000000..adf7f5b56
--- /dev/null
+++ b/multisrc/overrides/mymangacms/lkdtt/src/LKDTT.kt
@@ -0,0 +1,121 @@
+package eu.kanade.tachiyomi.extension.vi.lkdtt
+
+import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+class LKDTT : MyMangaCMS("LKDTT", "https://lkdtt.com", "vi") {
+ override val dateFormatter = SimpleDateFormat("dd/MM/yy", Locale.US).apply {
+ timeZone = TimeZone.getTimeZone(super.timeZone)
+ }
+
+ override fun dateUpdatedParser(date: String): Long =
+ runCatching { super.dateUpdatedParser(date.split(" - ")[1]) }.getOrNull() ?: 0L
+
+ override fun getGenreList() = listOf(
+ Genre("Học đường", 1),
+ Genre("Hài hước", 2),
+ Genre("Cổ Đại", 3),
+ Genre("Hiện đại", 4),
+ Genre("Kinh dị", 5),
+ Genre("Tổng tài", 6),
+ Genre("Xuyên không", 7),
+ Genre("Manhua", 8),
+ Genre("Manhwa", 9),
+ Genre("Mystery", 10),
+ Genre("One shot", 11),
+ Genre("Smut", 12),
+ Genre("Webtoon", 13),
+ Genre("Yaoi", 14),
+ Genre("Yuri", 15),
+ Genre("Trinh Thám", 16),
+ Genre("Tình Cảm", 17),
+ Genre("Drama", 18),
+ Genre("Comedy", 19),
+ Genre("Fantasy", 20),
+ Genre("Novel", 21),
+ Genre("Action", 22),
+ Genre("Manga", 23),
+ Genre("Đam Mỹ", 24),
+ Genre("Trọng Sinh", 25),
+ Genre("Ngôn Tình", 26),
+ Genre("Phiêu Lưu", 27),
+ Genre("Boy Love", 28),
+ Genre("giới giải trí", 29),
+ Genre("đô thị", 30),
+ Genre("Romance", 31),
+ Genre("Đô Thị", 32),
+ Genre("Shoujo", 33),
+ Genre("Historical", 34),
+ Genre("Slice of life", 35),
+ Genre("Mature", 36),
+ Genre("GL", 37),
+ Genre("Adult", 38),
+ Genre("Huyền huyễn", 39),
+ Genre("Baby", 40),
+ Genre("Tragedy", 41),
+ Genre("Truyện Màu", 42),
+ Genre("School Life", 43),
+ Genre("Josei", 44),
+ Genre("Oneshot", 45),
+ Genre("Gender Bender", 46),
+ Genre("Nữ cường", 47),
+ Genre("Harem", 48),
+ Genre("Reverse Harem", 49),
+ Genre("Isekai", 50),
+ Genre("Adventure", 51),
+ Genre("Chuyển Sinh", 52),
+ Genre("Đại Nữ Chủ", 53),
+ Genre("Shounen", 54),
+ Genre("Sports", 55),
+ Genre("Sủng Ngọt", 56),
+ Genre("Truyện 18+", 57),
+ Genre("Trung Cổ", 58),
+ Genre("Ma Thuật", 59),
+ Genre("Webtoons", 60),
+ Genre("Xuyên", 61),
+ Genre("Ngôn", 62),
+ Genre("Tiểu Bạch Thỏ", 63),
+ Genre("Sủng", 65),
+ Genre("Trùng Sinh", 66),
+ Genre("Ma Cà Rồng", 67),
+ Genre("Tái Sinh", 68),
+ Genre("Quân Nhân", 69),
+ Genre("Showbiz", 70),
+ Genre("Comic", 71),
+ Genre("Phép Thuật", 72),
+ Genre("Psychological", 73),
+ Genre("Supernatural", 74),
+ Genre("Lãng Mạn", 75),
+ Genre("Gender", 76),
+ Genre("Bender", 77),
+ Genre("Vườn Trường", 78),
+ Genre("Magic", 79),
+ Genre("Nhân Thú", 80),
+ Genre("Soft Yaoi", 81),
+ Genre("Hôn Nhân Hợp Đồng", 82),
+ Genre("Cưới Trước Yêu Sau", 83),
+ Genre("Bi Kịch", 84),
+ Genre("Horror", 85),
+ Genre("Reincarnation", 86),
+ Genre("Hồi Sinh", 87),
+ Genre("Hoàng Gia", 88),
+ Genre("Giả Tưởng", 89),
+ Genre("Xuyên Sách", 90),
+ Genre("Hài", 91),
+ Genre("Ngọt", 92),
+ Genre("Nam Cường", 93),
+ Genre("Chủ Nam", 94),
+ Genre("Minh Tinh", 95),
+ Genre("Cổ Trang", 96),
+ Genre("Xuyên Game", 97),
+ Genre("Villainess", 98),
+ Genre("Cung Đấu", 99),
+ Genre("Hành Động", 100),
+ Genre("Truyện Tranh", 101),
+ Genre("Adaptation", 102),
+ Genre("Magi", 103),
+ Genre("Âu Cổ", 104),
+ )
+}
diff --git a/multisrc/overrides/mymangacms/phemanga/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mymangacms/phemanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..5f47cda3d
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mymangacms/phemanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..5622f03a6
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..b279d4d2c
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..fac42cf6d
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..96a63ca6e
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/res/web_hi_res_512.png b/multisrc/overrides/mymangacms/phemanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..e7546a5a7
Binary files /dev/null and b/multisrc/overrides/mymangacms/phemanga/res/web_hi_res_512.png differ
diff --git a/multisrc/overrides/mymangacms/phemanga/src/PheManga.kt b/multisrc/overrides/mymangacms/phemanga/src/PheManga.kt
new file mode 100644
index 000000000..f26e9ff44
--- /dev/null
+++ b/multisrc/overrides/mymangacms/phemanga/src/PheManga.kt
@@ -0,0 +1,68 @@
+package eu.kanade.tachiyomi.extension.vi.phemanga
+
+import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
+
+class PheManga : MyMangaCMS("Phê Manga", "https://phemanga.net", "vi") {
+ override fun dateUpdatedParser(date: String): Long =
+ runCatching { super.dateUpdatedParser(date.split(" - ")[1]) }.getOrNull() ?: 0L
+
+ override fun getGenreList() = listOf(
+ Genre("16+", 1),
+ Genre("18+", 2),
+ Genre("Action", 3),
+ Genre("Adult", 4),
+ Genre("Adventure", 5),
+ Genre("Anime", 6),
+ Genre("Comedy", 7),
+ Genre("Comic", 8),
+ Genre("Doujinshi", 9),
+ Genre("Drama", 10),
+ Genre("Ecchi", 11),
+ Genre("Fantasy", 13),
+ Genre("Full màu", 14),
+ Genre("Game", 15),
+ Genre("Gender Bender", 16),
+ Genre("Harem", 17),
+ Genre("Historical", 18),
+ Genre("Horror", 19),
+ Genre("Isekai/Dị giới/Trọng sinh", 20),
+ Genre("Josei", 21),
+ Genre("Live action", 22),
+ Genre("Magic", 23),
+ Genre("Manga", 24),
+ Genre("Manhua", 25),
+ Genre("Manhwa", 26),
+ Genre("Martial Arts", 27),
+ Genre("Mature", 28),
+ Genre("Mecha", 29),
+ Genre("Mystery", 30),
+ Genre("Nấu Ăn", 31),
+ Genre("Ngôn Tình", 32),
+ Genre("NTR", 33),
+ Genre("One shot", 34),
+ Genre("Psychological", 35),
+ Genre("Romance", 36),
+ Genre("School Life", 37),
+ Genre("Sci-fi", 38),
+ Genre("Seinen", 39),
+ Genre("Shoujo", 40),
+ Genre("Shoujo Ai", 41),
+ Genre("Shounen", 42),
+ Genre("Shounen Ai", 43),
+ Genre("Slice of life", 44),
+ Genre("Smut", 45),
+ Genre("Soft Yaoi", 46),
+ Genre("Soft Yuri", 47),
+ Genre("Sports", 48),
+ Genre("Supernatural", 49),
+ Genre("Tạp chí truyện tranh", 50),
+ Genre("Tragedy", 51),
+ Genre("Trap (Crossdressing)", 52),
+ Genre("Trinh Thám", 53),
+ Genre("Truyện scan", 54),
+ Genre("Tu chân - tu tiên", 55),
+ Genre("VnComic", 56),
+ Genre("Webtoon", 57),
+ Genre("Yuri", 58),
+ )
+}
diff --git a/src/vi/truyentranhlh/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from src/vi/truyentranhlh/res/mipmap-hdpi/ic_launcher.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-hdpi/ic_launcher.png
diff --git a/src/vi/truyentranhlh/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from src/vi/truyentranhlh/res/mipmap-mdpi/ic_launcher.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-mdpi/ic_launcher.png
diff --git a/src/vi/truyentranhlh/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from src/vi/truyentranhlh/res/mipmap-xhdpi/ic_launcher.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xhdpi/ic_launcher.png
diff --git a/src/vi/truyentranhlh/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from src/vi/truyentranhlh/res/mipmap-xxhdpi/ic_launcher.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/src/vi/truyentranhlh/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from src/vi/truyentranhlh/res/mipmap-xxxhdpi/ic_launcher.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/src/vi/truyentranhlh/res/web_hi_res_512.png b/multisrc/overrides/mymangacms/truyentranhlh/res/web_hi_res_512.png
similarity index 100%
rename from src/vi/truyentranhlh/res/web_hi_res_512.png
rename to multisrc/overrides/mymangacms/truyentranhlh/res/web_hi_res_512.png
diff --git a/multisrc/overrides/mymangacms/truyentranhlh/src/TruyenTranhLH.kt b/multisrc/overrides/mymangacms/truyentranhlh/src/TruyenTranhLH.kt
new file mode 100644
index 000000000..182c40d28
--- /dev/null
+++ b/multisrc/overrides/mymangacms/truyentranhlh/src/TruyenTranhLH.kt
@@ -0,0 +1,68 @@
+package eu.kanade.tachiyomi.extension.vi.truyentranhlh
+
+import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
+
+class TruyenTranhLH : MyMangaCMS("TruyenTranhLH", "https://truyentranhlh.net", "vi") {
+ override val id: Long = 7969606392351831672
+
+ override fun getGenreList() = listOf(
+ Genre("Action", 1),
+ Genre("Adult", 2),
+ Genre("Adventure", 3),
+ Genre("Anime", 4),
+ Genre("Chuyển Sinh", 5),
+ Genre("Cổ Đại", 6),
+ Genre("Comedy", 7),
+ Genre("Comic", 8),
+ Genre("Demons", 9),
+ Genre("Detective", 10),
+ Genre("Doujinshi", 11),
+ Genre("Drama", 12),
+ Genre("Đam Mỹ", 13),
+ Genre("Ecchi", 14),
+ Genre("Fantasy", 15),
+ Genre("Gender Bender", 16),
+ Genre("Harem", 17),
+ Genre("Historical", 18),
+ Genre("Horror", 19),
+ Genre("Huyền Huyễn", 20),
+ Genre("Isekai", 21),
+ Genre("Josei", 22),
+ Genre("Mafia", 23),
+ Genre("Magic", 24),
+ Genre("Manhua", 25),
+ Genre("Manhwa", 26),
+ Genre("Martial Arts", 27),
+ Genre("Mature", 28),
+ Genre("Military", 29),
+ Genre("Mystery", 30),
+ Genre("Ngôn Tình", 31),
+ Genre("One shot", 32),
+ Genre("Psychological", 33),
+ Genre("Romance", 34),
+ Genre("School Life", 35),
+ Genre("Sci-fi", 36),
+ Genre("Seinen", 37),
+ Genre("Shoujo", 38),
+ Genre("Shoujo Ai", 39),
+ Genre("Shounen", 40),
+ Genre("Shounen Ai", 41),
+ Genre("Slice of life", 42),
+ Genre("Smut", 43),
+ Genre("Sports", 44),
+ Genre("Supernatural", 45),
+ Genre("Tragedy", 46),
+ Genre("Trọng Sinh", 47),
+ Genre("Truyện Màu", 48),
+ Genre("Webtoon", 49),
+ Genre("Xuyên Không", 50),
+ Genre("Yaoi", 51),
+ Genre("Yuri", 52),
+ Genre("Mecha", 53),
+ Genre("Cooking", 54),
+ Genre("Trùng Sinh", 55),
+ Genre("Gourmet", 56),
+ Genre("Dark Fantasy", 57),
+ )
+}
+
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMS.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMS.kt
new file mode 100644
index 000000000..e75639780
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMS.kt
@@ -0,0 +1,334 @@
+package eu.kanade.tachiyomi.multisrc.mymangacms
+
+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 okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.jsoup.select.Evaluator
+import rx.Observable
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+import java.util.concurrent.TimeUnit
+
+abstract class MyMangaCMS(
+ override val name: String,
+ override val baseUrl: String,
+ override val lang: String
+) : ParsedHttpSource() {
+
+ override val supportsLatest = true
+
+ override val client = network.cloudflareClient.newBuilder().apply {
+ rateLimit(3, 1)
+ connectTimeout(1, TimeUnit.MINUTES)
+ readTimeout(1, TimeUnit.MINUTES)
+ writeTimeout(1, TimeUnit.MINUTES)
+ }.build()
+
+ override fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
+ add("Referer", "$baseUrl/")
+ add(
+ "User-Agent",
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0"
+ )
+ }
+
+ //region Source settings
+
+ open val timeZone = "Asia/Ho_Chi_Minh"
+
+ open val dateFormatter = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
+ timeZone = TimeZone.getTimeZone(this@MyMangaCMS.timeZone)
+ }
+
+ open fun dateUpdatedParser(date: String): Long =
+ runCatching { dateFormatter.parse(date)?.time }.getOrNull() ?: 0L
+
+ private val floatingNumberRegex = Regex("""([+-]?(?:[0-9]*[.])?[0-9]+)""")
+
+ /**
+ * Regex for extracting URL from CSS `background-image: url()` property.
+ *
+ * - `url\(` matches the opening `url(`
+ * - `['"]?` checks for the existence (or lack thereof) of single/double quotes
+ * - `(.*?)` captures everything up to but not including the next quote
+ * - `\)` to match the closing bracket.
+ */
+ private val backgroundImageRegex = Regex("""url\(['"]?(.*?)['"]?\)""")
+ //endregion
+
+ //region Popular
+
+ override fun popularMangaRequest(page: Int): Request = GET(
+ baseUrl.toHttpUrl().newBuilder().apply {
+ addPathSegment("tim-kiem")
+ addQueryParameter("sort", "top")
+ addQueryParameter("page", page.toString())
+ }.build().toString()
+ )
+
+ override fun popularMangaSelector(): String = "div.thumb-item-flow.col-6.col-md-2"
+
+ override fun popularMangaNextPageSelector(): String? =
+ "div.pagination_wrap a.paging_item:last-of-type:not(.disabled)"
+
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ setUrlWithoutDomain(element.select("a").first().attr("abs:href"))
+ title = element.select("div.thumb_attr.series-title a[title]").first().text()
+ thumbnail_url = element.select("div[data-bg]").first().attr("data-bg")
+ }
+ //endregion
+
+ //region Latest
+
+ override fun latestUpdatesRequest(page: Int): Request = GET(
+ baseUrl.toHttpUrl().newBuilder().apply {
+ addPathSegment("tim-kiem")
+ addQueryParameter("sort", "update")
+ addQueryParameter("page", page.toString())
+ }.build().toString()
+ )
+
+ override fun latestUpdatesSelector(): String = popularMangaSelector()
+
+ override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga =
+ popularMangaFromElement(element)
+ //endregion
+
+ //region Search
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ return when {
+ query.startsWith(PREFIX_URL_SEARCH) -> {
+ fetchMangaDetails(SManga.create().apply {
+ url = query.removePrefix(PREFIX_URL_SEARCH).trim().replace(baseUrl, "")
+ })
+ .map { MangasPage(listOf(it), false) }
+ }
+ else -> super.fetchSearchManga(page, query, filters)
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
+ GET(
+ baseUrl.toHttpUrl().newBuilder().apply {
+ val genres = mutableListOf()
+ val genresEx = mutableListOf()
+ addPathSegment("tim-kiem")
+ addQueryParameter("page", page.toString())
+ (if (filters.isEmpty()) getFilterList() else filters).forEach {
+ when (it) {
+ is GenreList -> it.state.forEach { genre ->
+ when (genre.state) {
+ Filter.TriState.STATE_INCLUDE -> genres.add(genre.id)
+ Filter.TriState.STATE_EXCLUDE -> genresEx.add(genre.id)
+ else -> {}
+ }
+ }
+ is Author -> if (it.state.isNotEmpty()) {
+ addQueryParameter("artist", it.state)
+ }
+ is Sort -> addQueryParameter("sort", it.toUriPart())
+ is Status -> if (it.state != 0) {
+ addQueryParameter("status", it.state.toString())
+ }
+ else -> {}
+ }
+ }
+ if (genresEx.isNotEmpty()) {
+ addQueryParameter("reject_genres", genresEx.joinToString(","))
+ }
+ if (genres.isNotEmpty()) {
+ addQueryParameter("accept_genres", genres.joinToString(","))
+ }
+ if (query.isNotEmpty()) {
+ addQueryParameter("q", query)
+ }
+ }.build().toString()
+ )
+
+ override fun searchMangaSelector(): String = popularMangaSelector()
+
+ override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
+
+ override fun searchMangaFromElement(element: Element): SManga =
+ popularMangaFromElement(element)
+ //endregion
+
+ //region Manga details
+
+ override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}")
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ setUrlWithoutDomain(
+ document.select(".series-name-group a")
+ .first()
+ .attr("abs:href")
+ )
+ title = document.select(".series-name").first().text().trim()
+
+ var alternativeNames: String? = null
+ document.select(".info-item").forEach {
+ val value = it.select(".info-value")
+ when (it.select(".info-name").text().trim()) {
+ "Tên khác:" -> alternativeNames = value.joinToString(", ") { name ->
+ name.text().trim()
+ }
+ "Tác giả:" -> author = value.joinToString(", ") { auth ->
+ auth.text().trim()
+ }
+ "Tình trạng:" -> status = when (value.first().text().lowercase().trim()) {
+ "đang tiến hành" -> SManga.ONGOING
+ "tạm ngưng" -> SManga.ON_HIATUS
+ "đã hoàn thành" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+ }
+
+ val descElem = document.select(".summary-content")
+ description = if (descElem.select("p").any()) {
+ descElem.select("p").joinToString("\n") {
+ it.run {
+ select(Evaluator.Tag("br")).prepend("\\n")
+ this.text()
+ .replace("\\n", "\n")
+ .replace("\n ", "\n")
+ }
+ }.trim()
+ } else {
+ descElem.text().trim()
+ }
+
+ if (!alternativeNames.isNullOrEmpty()) {
+ description = "Tên khác: ${alternativeNames}\n\n" + description
+ }
+
+ genre = document.select("a[href*=the-loai] span.badge")
+ .joinToString(", ") { it.text().trim() }
+
+ thumbnail_url = document
+ .select("div.content.img-in-ratio")
+ .first()
+ .attr("style")
+ .let { backgroundImageRegex.find(it)?.groups?.get(1)?.value }
+ }
+ //endregion
+
+ //region Chapter list
+
+ override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
+
+ override fun chapterListSelector(): String = "ul.list-chapters > a"
+
+ override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
+
+ private fun chapterFromElement(element: Element, scanlator: String?): SChapter =
+ SChapter.create().apply {
+ setUrlWithoutDomain(element.attr("abs:href"))
+ name = element.select("div.chapter-name").first().text()
+ date_upload = dateUpdatedParser(
+ element.select("div.chapter-time").first().text()
+ )
+
+ val match = floatingNumberRegex.find(name)
+ chapter_number = if (name.lowercase().startsWith("vol")) {
+ match?.groups?.get(2)
+ } else {
+ match?.groups?.get(1)
+ }?.value?.toFloat() ?: -1f
+
+ this.scanlator = scanlator
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+ val originalScanlator = document.select("div.fantrans-value a")
+ val scanlator: String? = if (originalScanlator.isEmpty() ||
+ originalScanlator.first().text().trim().lowercase() == "đang cập nhật") {
+ null
+ } else {
+ originalScanlator.first().text().trim()
+ }
+
+ return document.select(chapterListSelector()).map { chapterFromElement(it, scanlator) }
+ }
+ //endregion
+
+ //region Pages
+
+ override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}")
+
+ override fun pageListParse(document: Document): List =
+ document
+ .select("div#chapter-content img")
+ .filterNot { it.attr("abs:data-src").isNullOrEmpty() }
+ .mapIndexed { index, elem -> Page(index, "", elem.attr("abs:data-src")) }
+
+ override fun imageUrlParse(document: Document): String = throw Exception("Not used")
+ //endregion
+
+ //region Filters
+ open class UriPartFilter(
+ displayName: String,
+ private val vals: Array>,
+ state: Int = 0,
+ ) : Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) {
+ fun toUriPart() = vals[state].second
+ }
+ private class Status : Filter.Select(
+ "Tình trạng",
+ arrayOf(
+ "Tất cả",
+ "Đang tiến hành",
+ "Tạm ngưng",
+ "Hoàn thành"
+ )
+ )
+ private class Sort : UriPartFilter(
+ "Sắp xếp",
+ arrayOf(
+ Pair("A-Z", "az"),
+ Pair("Z-A", "za"),
+ Pair("Mới cập nhật", "update"),
+ Pair("Truyện mới", "new"),
+ Pair("Xem nhiều", "top"),
+ Pair("Được thích nhiều", "like"),
+ ),
+ 4
+ )
+ open class Genre(name: String, val id: Int) : Filter.TriState(name)
+ private class Author : Filter.Text("Tác giả")
+ private class GenreList(genres: List) : Filter.Group("Thể loại", genres)
+
+ override fun getFilterList(): FilterList = FilterList(
+ Author(),
+ Status(),
+ Sort(),
+ GenreList(getGenreList()),
+ )
+
+ // To populate this list:
+ // console.log([...document.querySelectorAll("div.search-gerne_item")].map(elem => `Genre("${elem.textContent.trim()}", ${elem.querySelector("label").getAttribute("data-genre-id")}),`).join("\n"))
+ abstract fun getGenreList(): List
+ //endregion
+
+ companion object {
+ const val PREFIX_URL_SEARCH = "url:"
+ }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSGenerator.kt
new file mode 100644
index 000000000..3d0f4e684
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSGenerator.kt
@@ -0,0 +1,38 @@
+package eu.kanade.tachiyomi.multisrc.mymangacms
+
+import generator.ThemeSourceData.SingleLang
+import generator.ThemeSourceGenerator
+
+class MyMangaCMSGenerator : ThemeSourceGenerator {
+
+ override val themePkg = "mymangacms"
+
+ override val themeClass = "MyMangaCMS"
+
+ override val baseVersionCode: Int = 1
+
+ override val sources = listOf(
+ SingleLang(
+ "TruyenTranhLH",
+ "https://truyentranhlh.net",
+ "vi",
+ overrideVersionCode = 9
+ ),
+ SingleLang(
+ "Phê Manga",
+ "https://phemanga.net",
+ "vi",
+ true,
+ "PheManga",
+ "phemanga",
+ ),
+ SingleLang("LKDTT", "https://lkdtt.com", "vi", true)
+ )
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array) {
+ MyMangaCMSGenerator().createAll()
+ }
+ }
+}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSUrlActivity.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSUrlActivity.kt
new file mode 100644
index 000000000..0f3d43f3e
--- /dev/null
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mymangacms/MyMangaCMSUrlActivity.kt
@@ -0,0 +1,31 @@
+package eu.kanade.tachiyomi.multisrc.mymangacms
+
+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 MyMangaCMSUrlActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ try {
+ startActivity(Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${MyMangaCMS.PREFIX_URL_SEARCH}${intent?.data?.path}")
+ putExtra("filter", packageName)
+ })
+ } catch (e: ActivityNotFoundException) {
+ Log.e("MyMangaCMSUrlActivity", e.toString())
+ }
+ } else {
+ Log.e("MyMangaCMSUrlActivity", "Could not parse URI from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}
diff --git a/src/vi/truyentranhlh/AndroidManifest.xml b/src/vi/truyentranhlh/AndroidManifest.xml
deleted file mode 100644
index 30deb7f79..000000000
--- a/src/vi/truyentranhlh/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/vi/truyentranhlh/build.gradle b/src/vi/truyentranhlh/build.gradle
deleted file mode 100644
index 252e4c958..000000000
--- a/src/vi/truyentranhlh/build.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-
-ext {
- extName = 'TruyenTranhLH'
- pkgNameSuffix = 'vi.truyentranhlh'
- extClass = '.TruyenTranhLH'
- extVersionCode = 9
-}
-
-apply from: "$rootDir/common.gradle"
diff --git a/src/vi/truyentranhlh/src/eu/kanade/tachiyomi/extension/vi/truyentranhlh/TruyenTranhLH.kt b/src/vi/truyentranhlh/src/eu/kanade/tachiyomi/extension/vi/truyentranhlh/TruyenTranhLH.kt
deleted file mode 100644
index 084c7a143..000000000
--- a/src/vi/truyentranhlh/src/eu/kanade/tachiyomi/extension/vi/truyentranhlh/TruyenTranhLH.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package eu.kanade.tachiyomi.extension.vi.truyentranhlh
-
-import eu.kanade.tachiyomi.network.GET
-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.Headers
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.concurrent.TimeUnit
-
-class TruyenTranhLH : ParsedHttpSource() {
-
- override val name = "TruyenTranhLH"
-
- override val baseUrl = "https://truyentranhlh.net"
-
- override val lang = "vi"
-
- override val supportsLatest = true
-
- override val client: OkHttpClient = network.cloudflareClient.newBuilder()
- .connectTimeout(1, TimeUnit.MINUTES)
- .readTimeout(1, TimeUnit.MINUTES)
- .writeTimeout(1, TimeUnit.MINUTES)
- .build()
-
- override fun headersBuilder(): Headers.Builder = Headers.Builder()
- .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0")
-
- // Popular
-
- override fun popularMangaRequest(page: Int): Request {
- return GET("$baseUrl/tim-kiem?sort=top&page=$page", headers)
- }
-
- override fun popularMangaSelector() = "div.thumb-item-flow"
-
- override fun popularMangaFromElement(element: Element): SManga {
- return SManga.create().apply {
- element.select("div.series-title a").let {
- title = it.text()
- setUrlWithoutDomain(it.attr("href"))
- }
- thumbnail_url = element.select("div.content").attr("abs:data-bg")
- }
- }
-
- override fun popularMangaNextPageSelector() = "div.pagination_wrap a.page_num.current + a:not(.disabled)"
-
- // Latest
-
- override fun latestUpdatesRequest(page: Int): Request {
- return GET("$baseUrl/tim-kiem?sort=update&page=$page", headers)
- }
-
- override fun latestUpdatesSelector() = popularMangaSelector()
-
- override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
-
- override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
-
- // Search
-
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- return GET("$baseUrl/tim-kiem?q=$query&sort=update&page=$page", headers)
- }
-
- override fun searchMangaSelector() = popularMangaSelector()
-
- override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
-
- override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
-
- // Details
-
- override fun mangaDetailsParse(document: Document): SManga {
- val infoElement = document.select("div.top-part")
- return SManga.create().apply {
- genre = infoElement.select("span.info-name:contains(Thể loại) + span a").joinToString { it.text() }
- author = infoElement.select("span.info-name:contains(Tác giả) + span").text()
- status = infoElement.select("span.info-name:contains(Tình trạng) + span").text().toStatus()
- thumbnail_url = infoElement.select("div.content").attr("style")
- .let { Regex("""url\("(.*)"\)""").find(it)?.groups?.get(1)?.value }
- description = document.select("div.summary-content").text()
- }
- }
-
- private fun String?.toStatus() = when {
- this == null -> SManga.UNKNOWN
- this.contains("Đang tiến hành", ignoreCase = true) -> SManga.ONGOING
- this.contains("Đã hoàn thành", ignoreCase = true) -> SManga.COMPLETED
- else -> SManga.UNKNOWN
- }
-
- // Chapters
-
- override fun chapterListSelector(): String = "ul.list-chapters a"
-
- override fun chapterFromElement(element: Element): SChapter {
- return SChapter.create().apply {
- setUrlWithoutDomain(element.attr("href"))
- name = element.select("div.chapter-name").text()
- date_upload = element.select("div.chapter-time").firstOrNull()?.text()
- ?.let { SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).parse(it)?.time ?: 0L } ?: 0
- }
- }
-
- // Pages
-
- override fun pageListParse(document: Document): List {
- return document.select("div#chapter-content img")
- .filterNot { imgEl -> imgEl.attr("abs:data-src").isNullOrEmpty() }
- .mapIndexed { i, img ->
- Page(i, "", img.attr("abs:data-src"))
- }
- }
-
- override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
-}