diff --git a/src/vi/sayhentai/AndroidManifest.xml b/src/vi/sayhentai/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/src/vi/sayhentai/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/vi/sayhentai/build.gradle b/src/vi/sayhentai/build.gradle new file mode 100644 index 000000000..13a67cdd4 --- /dev/null +++ b/src/vi/sayhentai/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = "SayHentai" + extClass = ".SayHentai" + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/vi/sayhentai/res/mipmap-hdpi/ic_launcher.png b/src/vi/sayhentai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..da6673146 Binary files /dev/null and b/src/vi/sayhentai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/vi/sayhentai/res/mipmap-mdpi/ic_launcher.png b/src/vi/sayhentai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..460754a61 Binary files /dev/null and b/src/vi/sayhentai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/vi/sayhentai/res/mipmap-xhdpi/ic_launcher.png b/src/vi/sayhentai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7b7cc1944 Binary files /dev/null and b/src/vi/sayhentai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/vi/sayhentai/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/sayhentai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..4344a3d01 Binary files /dev/null and b/src/vi/sayhentai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/vi/sayhentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/sayhentai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..348026c0e Binary files /dev/null and b/src/vi/sayhentai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/vi/sayhentai/src/eu/kanade/tachiyomi/extension/vi/sayhentai/SayHentai.kt b/src/vi/sayhentai/src/eu/kanade/tachiyomi/extension/vi/sayhentai/SayHentai.kt new file mode 100644 index 000000000..ca87cd674 --- /dev/null +++ b/src/vi/sayhentai/src/eu/kanade/tachiyomi/extension/vi/sayhentai/SayHentai.kt @@ -0,0 +1,228 @@ +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 + +// 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.fun" + + 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 -> { + val genre = it.values[it.state] + addPathSegment(genre.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 { + 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.fun/", "")}"),`).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) : Filter.Select("Thể loại", genres) +}