diff --git a/src/vi/truyentranh8/AndroidManifest.xml b/src/vi/truyentranh8/AndroidManifest.xml new file mode 100644 index 000000000..aeb21ac92 --- /dev/null +++ b/src/vi/truyentranh8/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="eu.kanade.tachiyomi.extension"> + + <application> + <activity + android:name=".vi.truyentranh8.TruyenTranh8UrlActivity" + android:excludeFromRecents="true" + android:exported="true" + android:theme="@android:style/Theme.NoDisplay"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="truyentranh86.com" + android:pathPattern="/truyen-tranh/..*" + android:scheme="http" /> + </intent-filter> + </activity> + </application> +</manifest> \ No newline at end of file diff --git a/src/vi/truyentranh8/build.gradle b/src/vi/truyentranh8/build.gradle new file mode 100644 index 000000000..ad2d7c42c --- /dev/null +++ b/src/vi/truyentranh8/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Truyện Tranh 8' + pkgNameSuffix = 'vi.truyentranh8' + extClass = '.TruyenTranh8' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/vi/truyentranh8/res/mipmap-hdpi/ic_launcher.png b/src/vi/truyentranh8/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d3256b65e Binary files /dev/null and b/src/vi/truyentranh8/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/vi/truyentranh8/res/mipmap-mdpi/ic_launcher.png b/src/vi/truyentranh8/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..80504a3cd Binary files /dev/null and b/src/vi/truyentranh8/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/vi/truyentranh8/res/mipmap-xhdpi/ic_launcher.png b/src/vi/truyentranh8/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d550acddd Binary files /dev/null and b/src/vi/truyentranh8/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/vi/truyentranh8/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/truyentranh8/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6183b98cd Binary files /dev/null and b/src/vi/truyentranh8/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/vi/truyentranh8/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/truyentranh8/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..6ee854e8c Binary files /dev/null and b/src/vi/truyentranh8/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/vi/truyentranh8/res/web_hi_res_512.png b/src/vi/truyentranh8/res/web_hi_res_512.png new file mode 100644 index 000000000..0f1149239 Binary files /dev/null and b/src/vi/truyentranh8/res/web_hi_res_512.png differ diff --git a/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8.kt b/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8.kt new file mode 100644 index 000000000..54218c9e9 --- /dev/null +++ b/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8.kt @@ -0,0 +1,404 @@ +package eu.kanade.tachiyomi.extension.vi.truyentranh8 + +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 okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +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 + +class TruyenTranh8 : ParsedHttpSource() { + + override val name = "Truyện Tranh 8" + + override val baseUrl = "http://truyentranh86.com" + + override val lang = "vi" + + override val supportsLatest = true + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(1, 2, TimeUnit.SECONDS) + .build() + + override fun headersBuilder() = Headers.Builder() + .add("Referer", "$baseUrl/") + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0") + + private val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).apply { + timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") + } + + private val floatingNumberRegex = Regex("""([+-]?(?:[0-9]*[.])?[0-9]+)""") + + override fun popularMangaRequest(page: Int) = GET( + baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search.php") + addQueryParameter("act", "search") + addQueryParameter("sort", "xem") + addQueryParameter("view", "thumb") + addQueryParameter("page", page.toString()) + }.build().toString(), + headers + ) + + override fun popularMangaNextPageSelector(): String = "div#tblChap p.page a:contains(Cuối)" + + override fun popularMangaSelector(): String = "div#tblChap figure.col" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + setUrlWithoutDomain(element.select("figcaption h3 a").first().attr("href")) + title = element.select("figcaption h3 a").first().text().replace("[TT8] ", "") + thumbnail_url = element.select("img").first().attr("abs:src") + } + + override fun latestUpdatesRequest(page: Int) = GET( + baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search.php") + addQueryParameter("act", "search") + addQueryParameter("sort", "chap") + addQueryParameter("view", "thumb") + addQueryParameter("page", page.toString()) + }.build().toString(), + headers + ) + + override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector() + + override fun latestUpdatesSelector(): String = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { + return when { + query.startsWith(PREFIX_ID_SEARCH) -> { + val id = query.removePrefix(PREFIX_ID_SEARCH).trim() + if (id.isEmpty()) { + throw Exception("ID tìm kiếm không hợp lệ.") + } + fetchMangaDetails(SManga.create().apply { url = "/truyen-tranh/$id/" }) + .map { MangasPage(listOf(it), false) } + } + else -> super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = GET( + baseUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search.php") + addQueryParameter("act", "timnangcao") + addQueryParameter("view", "thumb") + addQueryParameter("page", page.toString()) + + if (query.isNotEmpty()) { + addQueryParameter("q", query) + } + + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is SortByFilter -> addQueryParameter("sort", filter.toUriPart()) + is SearchTypeFilter -> addQueryParameter("andor", filter.toUriPart()) + is ForFilter -> if (filter.state != 0) { + addQueryParameter("danhcho", filter.toUriPart()) + } + is AgeFilter -> if (filter.state != 0) { + addQueryParameter("DoTuoi", filter.toUriPart()) + } + is StatusFilter -> if (filter.state != 0) { + addQueryParameter("TinhTrang", filter.toUriPart()) + } + is OriginFilter -> if (filter.state != 0) { + addQueryParameter("quocgia", filter.toUriPart()) + } + is ReadingModeFilter -> if (filter.state != 0) { + addQueryParameter("KieuDoc", filter.toUriPart()) + } + is YearFilter -> if (filter.state.isNotEmpty()) { + addQueryParameter("NamPhaHanh", filter.state) + } + is UserFilter -> if (filter.state.isNotEmpty()) { + addQueryParameter("u", filter.state) + } + is AuthorFilter -> if (filter.state.isNotEmpty()) { + addQueryParameter("TacGia", filter.state) + } + is SourceFilter -> if (filter.state.isNotEmpty()) { + addQueryParameter("Nguon", filter.state) + } + is GenreList -> { + addQueryParameter( + "baogom", + filter.state + .filter { it.state == Filter.TriState.STATE_INCLUDE } + .joinToString(",") { it.id } + ) + addQueryParameter( + "khonggom", + filter.state + .filter { it.state == Filter.TriState.STATE_EXCLUDE } + .joinToString(",") { it.id } + ) + } + else -> {} + } + } + }.build().toString(), + headers + ) + + override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector() + + override fun searchMangaSelector(): String = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.select("h1.fs-5").first().text().replace("Truyện Tranh ", "") + + author = document.select("span[itemprop=author]") + .filter { it.text().isNotEmpty() } + .joinToString(", ") { it.text() } + + thumbnail_url = document.select("img.thumbnail").first().attr("abs:src") + + genre = document.select("a[itemprop=genre]") + .filter { it.text().isNotEmpty() } + .joinToString(", ") { it.text() } + + status = when (document.select("ul.mangainfo b:contains(Tình Trạng) + a").first().text().trim()) { + "Đang tiến hành" -> SManga.ONGOING + "Đã hoàn thành" -> SManga.COMPLETED + "Tạm ngưng" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + + val descnode = document.select("div.card-body.border-start.border-info.border-3").first() + descnode.select(Evaluator.Tag("br")).prepend("\\n") + + description = if (descnode.select("p").any()) { + descnode.select("p").joinToString("\n") { + it.text().replace("\\n", "\n").replace("\n ", "\n") + }.trim() + } else { + descnode.text().replace("\\n", "\n").replace("\n ", "\n").trim() + } + } + + override fun chapterListSelector() = "ul#ChapList li" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.select("a").first().attr("abs:href")) + name = element.text().replace(element.select("time").first().text(), "") + date_upload = runCatching { + dateFormatter.parse(element.select("time").first().attr("datetime"))?.time + }.getOrNull() ?: 0L + + val match = floatingNumberRegex.find(name) + chapter_number = if (name.lowercase().startsWith("vol")) { + match?.groups?.get(2) + } else { + match?.groups?.get(1) + }?.value?.toFloat() ?: -1f + } + + override fun pageListParse(document: Document) = document.select("div.page-chapter") + .mapIndexed { i, elem -> + Page(i, "", elem.select("img").first().attr("abs:src")) + } + + override fun imageUrlParse(document: Document): String = throw Exception("Not used") + + open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>, state: Int = 0) : + Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) { + fun toUriPart() = vals[state].second + } + + private class YearFilter : Filter.Text("Năm phát hành") + private class UserFilter : Filter.Text("Đăng bởi thành viên") + private class AuthorFilter : Filter.Text("Tên tác giả") + private class SourceFilter : Filter.Text("Nguồn/Nhóm dịch") + private class SearchTypeFilter : UriPartFilter( + "Kiểu tìm", + arrayOf( + Pair("AND/và", "and"), + Pair("OR/hoặc", "or"), + ) + ) + private class ForFilter : UriPartFilter( + "Dành cho", + arrayOf( + Pair("Bất kì", ""), + Pair("Con gái", "gai"), + Pair("Con trai", "trai"), + Pair("Con nít", "nit"), + ) + ) + private class AgeFilter : UriPartFilter( + "Bất kỳ", + arrayOf( + Pair("Bất kì", ""), + Pair("= 13", "13"), + Pair("= 14", "14"), + Pair("= 15", "15"), + Pair("= 16", "16"), + Pair("= 17", "17"), + Pair("= 18", "18"), + ) + ) + private class StatusFilter : UriPartFilter( + "Tình trạng", + arrayOf( + Pair("Bất kì", ""), + Pair("Đang dịch", "Ongoing"), + Pair("Hoàn thành", "Complete"), + Pair("Tạm ngưng", "Drop"), + ) + ) + private class OriginFilter : UriPartFilter( + "Quốc gia", + arrayOf( + Pair("Bất kì", ""), + Pair("Nhật Bản", "nhat"), + Pair("Trung Quốc", "trung"), + Pair("Hàn Quốc", "han"), + Pair("Việt Nam", "vietnam"), + ) + ) + private class ReadingModeFilter : UriPartFilter( + "Kiểu đọc", + arrayOf( + Pair("Bất kì", ""), + Pair("Chưa xác định", "chưa xác định"), + Pair("Phải qua trái", "xem từ phải qua trái"), + Pair("Trái qua phải", "xem từ trái qua phải"), + ) + ) + private class SortByFilter : UriPartFilter( + "Sắp xếp theo", + arrayOf( + Pair("Chap mới", "chap"), + Pair("Truyện mới", "truyen"), + Pair("Xem nhiều", "xem"), + Pair("Theo ABC", "ten"), + Pair("Số Chương", "sochap"), + ), + 2 + ) + open class Genre(name: String, val id: String) : Filter.TriState(name) + private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres) + override fun getFilterList() = FilterList( + GenreList(getGenreList()), + SortByFilter(), + SearchTypeFilter(), + ForFilter(), + AgeFilter(), + StatusFilter(), + OriginFilter(), + ReadingModeFilter(), + YearFilter(), + UserFilter(), + AuthorFilter(), + SourceFilter(), + ) + + private fun getGenreList() = listOf( + Genre("Phát Hành Tại TT8", "106"), + Genre("Truyện Màu", "113"), + Genre("Webtoons", "112"), + Genre("Manga - Truyện Nhật", "141"), + Genre("Action - Hành động", "52"), + Genre("Adult - Người lớn", "53"), + Genre("Adventure - Phiêu lưu", "65"), + Genre("Anime", "107"), + Genre("Biseinen", "123"), + Genre("Bishounen", "122"), + Genre("Comedy - Hài hước", "50"), + Genre("Doujinshi", "72"), + Genre("Drama", "73"), + Genre("Ecchi", "74"), + Genre("Fantasy", "75"), + Genre("Gender Bender - Đổi giới tính", "76"), + Genre("Harem", "77"), + Genre("Historical - Lịch sử", "78"), + Genre("Horror - Kinh dị", "79"), + Genre("Isekai - Xuyên không", "139"), + Genre("Josei", "80"), + Genre("Live-action - Live Action", "81"), + Genre("Macgic", "138"), + Genre("Magic - Phép thuật", "116"), + Genre("Martial Arts - Martial-Arts", "84"), + Genre("Mature - Trưởng thành", "85"), + Genre("Mecha - Robot", "86"), + Genre("Mystery - Bí ẩn", "87"), + Genre("One-shot", "88"), + Genre("Psychological - Tâm lý", "89"), + Genre("Romance - Tình cảm", "90"), + Genre("School Life - Học đường", "91"), + Genre("Sci fi - Khoa học viễn tưởng", "92"), + Genre("Seinen", "93"), + Genre("Shoujo", "94"), + Genre("Shoujo Ai", "66"), + Genre("Shounen", "96"), + Genre("Shounen Ai", "97"), + Genre("Slash", "121"), + Genre("Slice-of-Life - Đời sống", "98"), + Genre("Smut", "99"), + Genre("Soft Yaoi - Soft-Yaoi", "100"), + Genre("Sports - Thể thao", "101"), + Genre("Supernatural - Siêu nhiên", "102"), + Genre("Tạp chí truyện tranh", "103"), + Genre("Tragedy - Bi kịch", "104"), + Genre("Trap - Crossdressing", "115"), + Genre("Yaoi", "114"), + Genre("Yaoi Hardcore", "120"), + Genre("Yuri", "111"), + Genre("Manhua - Truyện Trung", "82"), + Genre("Bách Hợp", "128"), + Genre("Chuyển sinh", "134"), + Genre("Cổ đại", "135"), + Genre("Cung đình", "144"), + Genre("Giới giải trí", "146"), + Genre("Hậu cung", "145"), + Genre("Huyền Huyễn", "132"), + Genre("Khoa Huyễn", "130"), + Genre("Lịch Sử", "131"), + Genre("Ngôn tình", "127"), + Genre("Ngọt sủng", "148"), + Genre("Ngược", "143"), + Genre("Người đóng góp", "147"), + Genre("Nữ Cường", "136"), + Genre("Tổng tài", "137"), + Genre("Trọng Sinh", "126"), + Genre("Trường học", "142"), + Genre("Tu chân - tu tiên", "140"), + Genre("Võng Du", "125"), + Genre("Xuyên không", "124"), + Genre("Đam Mỹ", "108"), + Genre("Đô thị", "129"), + Genre("Manhwa - Truyện Hàn", "83"), + Genre("Boy love", "133"), + Genre("Thriller - Giết người, sát nhân, máu me", "149"), + Genre("Truyện Tranh Việt", "51"), + Genre("Cướp bồ - NTR, Netorare", "118"), + Genre("Hướng dẫn vẽ!", "109"), + Genre("Truyện scan", "105"), + Genre("Comic - truyện Âu Mĩ", "71"), + ) + + companion object { + const val PREFIX_ID_SEARCH = "id:" + } +} diff --git a/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8UrlActivity.kt b/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8UrlActivity.kt new file mode 100644 index 000000000..7e3694f00 --- /dev/null +++ b/src/vi/truyentranh8/src/eu/kanade/tachiyomi/extension/vi/truyentranh8/TruyenTranh8UrlActivity.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.extension.vi.truyentranh8 + +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 TruyenTranh8UrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + + try { + startActivity( + Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${TruyenTranh8.PREFIX_ID_SEARCH}$id") + putExtra("filter", packageName) + } + ) + } catch (e: ActivityNotFoundException) { + Log.e("TruyenTranh8UrlActivity", e.toString()) + } + } else { + Log.e("TruyenTranh8UrlActivity", "Could not parse URL from intent $intent") + } + + finish() + exitProcess(0) + } +}