Make ManhwaZ a multisrc + Add UmeTruyen (#1495)

* Make ManhwaZ a multisrc + Add UmeTruyen

* Forgot to commit the most important stuff

* icons

* Update src/en/manhwaz/build.gradle

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Don't use GlobalScope

* Remove useless optin

* Add CoroutineScope import

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
beerpsi 2024-02-26 19:46:07 +07:00 committed by GitHub
parent b95ee17453
commit 4cbfd4b720
16 changed files with 358 additions and 426 deletions

View File

@ -0,0 +1,12 @@
filter_ignored_warning=Ignored when using text search
cannot_use_order_by_warning=Cannot use "Order by" filter when genre is "%s" or "%s"
genre_fetch_failed=Failed to fetch genres
genre_missing_warning=Press "Reset" to attempt to show genres
genre_filter_title=Genre
genre_all=All
genre_completed=Completed
order_by_filter_title=Order by
order_by_latest=Latest
order_by_rating=Rating
order_by_most_views=Most views
order_by_new=New

View File

@ -0,0 +1,12 @@
filter_ignored_warning=Không thể dùng chung với tìm kiếm bằng từ khoá
cannot_use_order_by_warning=Không thể sắp xếp nếu chọn thể loại là "%s" hoặc "%s"
genre_fetch_failed=Đã có lỗi khi tải thể loại
genre_missing_warning=Chọn "Đặt lại" để hiển thị thể loại
genre_filter_title=Thể loại
genre_all=Tất cả
genre_completed=Hoàn thành
order_by_filter_title=Sắp xếp theo
order_by_latest=Mới nhất
order_by_rating=Đánh giá cao
order_by_most_views=Xem nhiều
order_by_new=Mới

View File

@ -0,0 +1,9 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1
dependencies {
api(project(":lib:i18n"))
}

View File

@ -0,0 +1,275 @@
package eu.kanade.tachiyomi.multisrc.manhwaz
import android.util.Log
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
abstract class ManhwaZ(
override val name: String,
override val baseUrl: String,
final override val lang: String,
private val mangaDetailsAuthorHeading: String = "author(s)",
private val mangaDetailsStatusHeading: String = "status",
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
protected val intl = Intl(
lang,
setOf("en", "vi"),
"en",
this::class.java.classLoader!!,
)
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = "#slide-top > .item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".info-item a")!!.let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
}
thumbnail_url = element.selectFirst(".img-item img")?.imgAttr()
}
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesSelector() = ".page-item-detail"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".item-summary a")!!.let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
}
thumbnail_url = element.selectFirst(".item-thumb img")?.imgAttr()
}
override fun latestUpdatesNextPageSelector(): String? = "ul.pager a[rel=next]"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("s", query)
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
val filterList = filters.ifEmpty { getFilterList() }
val genreFilter = filterList.find { it is GenreFilter } as? GenreFilter
val orderByFilter = filterList.find { it is OrderByFilter } as? OrderByFilter
val genreId = genreFilter?.options?.get(genreFilter.state)?.id
if (genreFilter != null && genreFilter.state != 0) {
addPathSegments(genreId!!)
}
// Can't sort in "All" or "Completed"
if (orderByFilter != null && genreId?.startsWith("genre/") == true) {
addQueryParameter(
"m_orderby",
orderByFilter.options[orderByFilter.state].id,
)
}
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
override fun searchMangaSelector() = latestUpdatesSelector()
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector(): String? = latestUpdatesNextPageSelector()
private val ongoingStatusList = listOf("ongoing", "đang ra")
private val completedStatusList = listOf("completed", "hoàn thành")
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val statusText = document.selectFirst("div.summary-heading:contains($mangaDetailsStatusHeading) + div.summary-content")
?.text()
?.lowercase()
?: ""
title = document.selectFirst("div.post-title h1")!!.text()
author = document.selectFirst("div.summary-heading:contains($mangaDetailsAuthorHeading) + div.summary-content")?.text()
description = document.selectFirst("div.summary__content")?.text()
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
status = when {
ongoingStatusList.contains(statusText) -> SManga.ONGOING
completedStatusList.contains(statusText) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = document.selectFirst("div.summary_image img")?.imgAttr()
}
override fun chapterListSelector() = "li.wp-manga-chapter"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
element.selectFirst("a")!!.let {
setUrlWithoutDomain(it.attr("href"))
name = it.text()
}
element.selectFirst("span.chapter-release-date")?.text()?.let {
date_upload = parseRelativeDate(it)
}
}
override fun pageListParse(document: Document) =
document.select("div.page-break img").mapIndexed { i, it ->
Page(i, imageUrl = it.imgAttr())
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
override fun getFilterList(): FilterList {
fetchGenreList()
val filters = buildList {
add(Filter.Header(intl["filter_ignored_warning"]))
add(Filter.Header(intl.format("cannot_use_order_by_warning", intl["genre_all"], intl["genre_completed"])))
if (fetchGenreStatus == FetchGenreStatus.NOT_FETCHED && fetchGenreAttempts >= 3) {
add(Filter.Header(intl["genre_fetch_failed"]))
} else if (fetchGenreStatus != FetchGenreStatus.FETCHED) {
add(Filter.Header(intl["genre_missing_warning"]))
}
add(Filter.Separator())
if (genres.isNotEmpty()) {
add(GenreFilter(intl, genres))
}
add(OrderByFilter(intl))
}
return FilterList(filters)
}
private class GenreFilter(
intl: Intl,
genres: List<SelectOption>,
) : SelectFilter(intl["genre_filter_title"], genres)
private class OrderByFilter(intl: Intl) : SelectFilter(
intl["order_by_filter_title"],
listOf(
SelectOption(intl["order_by_latest"], "latest"),
SelectOption(intl["order_by_rating"], "rating"),
SelectOption(intl["order_by_most_views"], "views"),
SelectOption(intl["order_by_new"], "new"),
),
)
private var genres = emptyList<SelectOption>()
private var fetchGenreStatus = FetchGenreStatus.NOT_FETCHED
private var fetchGenreAttempts = 0
private val scope = CoroutineScope(Dispatchers.IO)
private fun fetchGenreList() {
if (fetchGenreStatus != FetchGenreStatus.NOT_FETCHED || fetchGenreAttempts >= 3) {
return
}
fetchGenreStatus = FetchGenreStatus.FETCHING
fetchGenreAttempts++
scope.launch {
try {
val document = client.newCall(GET("$baseUrl/genre")).await().asJsoup()
genres = buildList {
add(SelectOption(intl["genre_all"], ""))
add(SelectOption(intl["genre_completed"], "completed"))
document.select("ul.page-genres li a").forEach {
val path = it.absUrl("href").toHttpUrl().encodedPath.removePrefix("/")
add(SelectOption(it.ownText(), path))
}
}
fetchGenreStatus = FetchGenreStatus.FETCHED
} catch (e: Exception) {
Log.e("ManhwaZ/$name", "Error fetching genres", e)
fetchGenreStatus = FetchGenreStatus.NOT_FETCHED
}
}
}
private enum class FetchGenreStatus { NOT_FETCHED, FETCHED, FETCHING }
private class SelectOption(val name: String, val id: String)
private open class SelectFilter(
name: String,
val options: List<SelectOption>,
) : Filter.Select<String>(name, options.map { it.name }.toTypedArray())
private val secondsUnit = listOf("second", "seconds", "giây")
private val minutesUnit = listOf("minute", "minutes", "phút")
private val hourUnit = listOf("hour", "hours", "giờ")
private val dayUnit = listOf("day", "days", "ngày")
private val weekUnit = listOf("week", "weeks", "tuần")
private val monthUnit = listOf("month", "months", "tháng")
private val yearUnit = listOf("year", "years", "năm")
private fun parseRelativeDate(date: String): Long {
val (valueString, unit) = date.substringBeforeLast(" ").split(" ", limit = 2)
val value = valueString.toInt()
val calendar = Calendar.getInstance().apply {
val field = when {
secondsUnit.contains(unit) -> Calendar.SECOND
minutesUnit.contains(unit) -> Calendar.MINUTE
hourUnit.contains(unit) -> Calendar.HOUR_OF_DAY
dayUnit.contains(unit) -> Calendar.DAY_OF_MONTH
weekUnit.contains(unit) -> Calendar.WEEK_OF_MONTH
monthUnit.contains(unit) -> Calendar.MONTH
yearUnit.contains(unit) -> Calendar.YEAR
else -> return 0L
}
add(field, -value)
}
return calendar.timeInMillis
}
protected fun Element.imgAttr(): String = when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
else -> attr("abs:src")
}
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'ManhwaZ'
extClass = '.ManhwaZ'
themePkg = 'madara'
extClass = '.ManhwaZCom'
themePkg = 'manhwaz'
baseUrl = 'https://manhwaz.com'
overrideVersionCode = 0
overrideVersionCode = 35
isNsfw = true
}

View File

@ -1,200 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhwaz
import eu.kanade.tachiyomi.multisrc.madara.Madara
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.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
class ManhwaZ : Madara(
"ManhwaZ",
"https://manhwaz.com",
"en",
) {
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(2)
.build()
override val fetchGenres = false
override val useNewChapterEndpoint = true
// Popular
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/", headers)
override fun popularMangaSelector(): String = "div#slide-top > div.item"
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst(".img-item img")?.let(::imageFromElement) ?: ""
element.selectFirst(".info-item a")!!.run {
title = text().trim()
setUrlWithoutDomain(attr("href"))
}
}
// Latest
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesSelector(): String = ".manga-content > div.row > div"
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst(".item-thumb img")?.let(::imageFromElement) ?: ""
element.selectFirst(".item-summary a")!!.run {
title = text().trim()
setUrlWithoutDomain(attr("href"))
}
}
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li.active + li:not(.disabled)"
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment("search")
addQueryParameter("s", query)
} else {
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
if (filter.selected == null) throw Exception("Must select a genre")
addPathSegment("genre")
addPathSegment(filter.selected!!)
}
is OrderFilter -> {
addQueryParameter("m_orderby", filter.selected)
}
else -> {}
}
}
}
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
return if (response.request.url.encodedPath.startsWith("/search")) {
searchParse(response)
} else {
super.searchMangaParse(response)
}
}
override fun searchMangaSelector(): String = "div.listing > div"
override fun searchMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector(): String = latestUpdatesNextPageSelector()
private fun searchParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangaList = document.select(".page-search > .container > .row > div")
.map(::searchMangaFromElement)
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(mangaList, hasNextPage)
}
// Filter
abstract class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
val selected get() = options[state].second.takeUnless { it.isEmpty() }
}
class OrderFilter : SelectFilter(
"Order By",
listOf(
Pair("Latest", "latest"),
Pair("Rating", "rating"),
Pair("Most Views", "views"),
Pair("New", "new"),
),
)
class GenreFilter : SelectFilter(
"Genre",
listOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adult", "adult"),
Pair("Adventure", "adventure"),
Pair("Comedy", "comedy"),
Pair("Cooking", "cooking"),
Pair("Detective", "detective"),
Pair("Doujinshi", "doujinshi"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fantasy", "fantasy"),
Pair("Gender Bender", "gender-bender"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Isekai", "isekai"),
Pair("Josei", "josei"),
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
Pair("Manhwa", "manhwa"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Mystery", "mystery"),
Pair("One shot", "one-shot"),
Pair("Psychological", "psychological"),
Pair("Romance", "romance"),
Pair("School Life", "school-life"),
Pair("Sci-fi", "sci-fi"),
Pair("Seinen", "seinen"),
Pair("Shoujo", "shoujo"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen", "shounen"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Smut", "smut"),
Pair("Sports", "sports"),
Pair("Supernatural", "supernatural"),
Pair("Tragedy", "tragedy"),
Pair("Webtoon", "webtoon"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
),
)
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
GenreFilter(),
OrderFilter(),
)
// Details
override val mangaDetailsSelectorStatus = ".post-content_item:contains(status) .summary-content"
override val mangaDetailsSelectorAuthor = ".post-content_item:contains(Author) .summary-content"
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.en.manhwaz
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import okhttp3.OkHttpClient
class ManhwaZCom : ManhwaZ(
"ManhwaZ",
"https://manhwaz.com",
"en",
) {
override val client: OkHttpClient = super.client.newBuilder()
.rateLimit(2)
.build()
}

View File

@ -1,7 +1,9 @@
ext {
extName = "SayHentai"
extClass = ".SayHentai"
extVersionCode = 3
themePkg = "manhwaz"
baseUrl = "https://sayhentai.club"
overrideVersionCode = 3
isNsfw = true
}

View File

@ -1,225 +1,11 @@
package eu.kanade.tachiyomi.extension.vi.sayhentai
import eu.kanade.tachiyomi.network.GET
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.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
// This is basically Madara CSS without the actual Madara bits, grrr
class SayHentai : ParsedHttpSource() {
override val name = "SayHentai"
override val lang = "vi"
override val baseUrl = "https://sayhentai.club"
override val supportsLatest = false
override fun headersBuilder() = super.headersBuilder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int) = GET("$baseUrl/?page=$page")
override fun popularMangaSelector() = "div.page-item-detail"
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")?.imageFromElement()
}
override fun popularMangaNextPageSelector() = "ul.pager a[rel=next]"
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment("search")
addQueryParameter("s", query)
} else {
(if (filters.isEmpty()) getFilterList() else filters).forEach {
when (it) {
is GenreList -> addPathSegments(it.values[it.state].path)
else -> {}
}
}
}
addQueryParameter("page", 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.summary-heading:contains(Tác giả) + div.summary-content")?.text()
description = document.selectFirst("div.summary__content")?.text()
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
status = when (document.selectFirst("div.summary-heading:contains(Trạng thái) + div.summary-content")?.text()) {
"Đang Ra" -> SManga.ONGOING
"Hoàn Thành" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = document.selectFirst("div.summary_image img")?.imageFromElement()
}
override fun chapterListSelector() = "li.wp-manga-chapter"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
val a = element.selectFirst("a")!!
val date = element.selectFirst("span.chapter-release-date")?.text()
setUrlWithoutDomain(a.attr("abs:href"))
name = a.text()
if (date != null) {
date_upload = parseRelativeDate(date)
}
}
override fun pageListParse(document: Document): List<Page> {
return document.select("div.page-break img").mapIndexed { i, it ->
Page(i, imageUrl = it.imageFromElement())
}
}
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á."),
GenreList(getGenreList()),
)
private fun Element.imageFromElement(): String? {
return when {
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
else -> attr("abs:src")
}
}
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
}
// document.querySelectorAll("span.number-story").forEach((e) => e.remove())
// copy([...document.querySelectorAll(".page-category ul li a")].map((e) => `Genre("${e.textContent.trim()}", "${e.href.replace("https://sayhentai.club/", "")}"),`).join("\n"))
//
// There are 2 pseudo-genres: Tất cả (All), and Hoàn thành (Completed), mostly for convenience.
private fun getGenreList() = arrayOf(
Genre("Tất cả", ""),
Genre("18+", "genre/18"),
Genre("3D", "genre/3d"),
Genre("Action", "genre/action"),
Genre("Adult", "genre/adult"),
Genre("Bạo Dâm", "genre/bao-dam"),
Genre("Chơi Hai Lỗ", "genre/choi-hai-lo"),
Genre("Comedy", "genre/comedy"),
Genre("Detective", "genre/detective"),
Genre("Doujinshi", "genre/doujinshi"),
Genre("Drama", "genre/drama"),
Genre("Ecchi", "genre/ecchi"),
Genre("Fantasy", "genre/fantasy"),
Genre("Gangbang", "genre/gangbang"),
Genre("Gender Bender", "genre/gender-bender"),
Genre("Giáo Viên", "genre/giao-vien"),
Genre("Group", "genre/group"),
Genre("Hãm Hiếp", "genre/ham-hiep"),
Genre("Harem", "genre/harem"),
Genre("Hậu Môn", "genre/hau-mon"),
Genre("Historical", "genre/historical"),
Genre("Hoàn thành", "completed"),
Genre("Horror", "genre/horror"),
Genre("Housewife", "genre/housewife"),
Genre("Josei", "genre/josei"),
Genre("Không Che", "genre/khong-che"),
Genre("Kinh Dị", "genre/kinh-di"),
Genre("Lão Già Dâm", "genre/lao-gia-dam"),
Genre("Loạn Luân", "genre/loan-luan"),
Genre("Loli", "genre/loli"),
Genre("Manga", "genre/manga"),
Genre("Manhua", "genre/manhua"),
Genre("Manhwa", "genre/manhwa"),
Genre("Martial Arts", "genre/martial-arts"),
Genre("Mature", "genre/mature"),
Genre("Milf", "genre/milf"),
Genre("Mind Break", "genre/mind-break"),
Genre("Mystery", "genre/mystery"),
Genre("Ngực Lớn", "genre/nguc-lon"),
Genre("Ngực Nhỏ", "genre/nguc-nho"),
Genre("Nô Lệ", "genre/no-le"),
Genre("NTR", "genre/ntr"),
Genre("Nữ Sinh", "genre/nu-sinh"),
Genre("Old Man", "genre/old-man"),
Genre("One shot", "genre/one-shot"),
Genre("Oneshot", "genre/oneshot"),
Genre("Psychological", "genre/psychological"),
Genre("Rape", "genre/rape"),
Genre("Romance", "genre/romance"),
Genre("School Life", "genre/school-life"),
Genre("Sci-fi", "genre/sci-fi"),
Genre("Seinen", "genre/seinen"),
Genre("Series", "genre/series"),
Genre("Shoujo", "genre/shoujo"),
Genre("Shoujo Ai", "genre/shoujo-ai"),
Genre("Shounen", "genre/shounen"),
Genre("Slice of Life", "genre/slice-of-life"),
Genre("Smut", "genre/smut"),
Genre("Sports", "genre/sports"),
Genre("Supernatural", "genre/supernatural"),
Genre("Tragedy", "genre/tragedy"),
Genre("Virgin", "genre/virgin"),
Genre("Webtoon", "genre/webtoon"),
Genre("Y Tá", "genre/y-ta"),
Genre("Yaoi", "genre/yaoi"),
Genre("Yuri", "genre/yuri"),
)
private class Genre(val name: String, val path: String) {
override fun toString() = name
}
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Thể loại", genres)
}
class SayHentai : ManhwaZ(
"SayHentai",
"https://sayhentai.club",
"vi",
mangaDetailsAuthorHeading = "Tác giả",
mangaDetailsStatusHeading = "Trạng thái",
)

View File

@ -0,0 +1,10 @@
ext {
extName = "UmeTruyen"
extClass = ".UmeTruyen"
themePkg = "manhwaz"
baseUrl = "https://umetruyenvip.com"
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.vi.umetruyen
import eu.kanade.tachiyomi.multisrc.manhwaz.ManhwaZ
class UmeTruyen : ManhwaZ(
"UmeTruyen",
"https://umetruyenvip.com",
"vi",
mangaDetailsAuthorHeading = "Tác giả",
mangaDetailsStatusHeading = "Trạng thái",
)