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) + } +}