diff --git a/src/vi/lxhentai/AndroidManifest.xml b/src/vi/lxhentai/AndroidManifest.xml new file mode 100644 index 000000000..14f660088 --- /dev/null +++ b/src/vi/lxhentai/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/vi/lxhentai/build.gradle b/src/vi/lxhentai/build.gradle new file mode 100644 index 000000000..86588bd15 --- /dev/null +++ b/src/vi/lxhentai/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'LXHentai' + pkgNameSuffix = 'vi.lxhentai' + extClass = '.LxHentai' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" \ No newline at end of file diff --git a/src/vi/lxhentai/res/mipmap-hdpi/ic_launcher.png b/src/vi/lxhentai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..537f29002 Binary files /dev/null and b/src/vi/lxhentai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/vi/lxhentai/res/mipmap-mdpi/ic_launcher.png b/src/vi/lxhentai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e1f58d8a2 Binary files /dev/null and b/src/vi/lxhentai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/vi/lxhentai/res/mipmap-xhdpi/ic_launcher.png b/src/vi/lxhentai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..33fc43b46 Binary files /dev/null and b/src/vi/lxhentai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/vi/lxhentai/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/lxhentai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..427aee160 Binary files /dev/null and b/src/vi/lxhentai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/vi/lxhentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/lxhentai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..739b0bc27 Binary files /dev/null and b/src/vi/lxhentai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/vi/lxhentai/res/web_hi_res_512.png b/src/vi/lxhentai/res/web_hi_res_512.png new file mode 100644 index 000000000..862ec419a Binary files /dev/null and b/src/vi/lxhentai/res/web_hi_res_512.png differ diff --git a/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentai.kt b/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentai.kt new file mode 100644 index 000000000..b5f0e7d1a --- /dev/null +++ b/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentai.kt @@ -0,0 +1,303 @@ +package eu.kanade.tachiyomi.extension.vi.lxhentai + +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.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 okhttp3.OkHttpClient +import okhttp3.Request +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 + +class LxHentai : ParsedHttpSource() { + + override val name = "LXHentai" + + override val baseUrl = "https://lxmanga.net" + + override val lang = "vi" + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl) + + override fun popularMangaRequest(page: Int) = + searchMangaRequest(page, "", FilterList(SortBy(3))) + + override fun popularMangaSelector() = searchMangaSelector() + + override fun popularMangaFromElement(element: Element) = + searchMangaFromElement(element) + + override fun popularMangaNextPageSelector() = + searchMangaNextPageSelector() + + override fun latestUpdatesRequest(page: Int) = + searchMangaRequest(page, "", FilterList(SortBy(0))) + + override fun latestUpdatesSelector() = searchMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = + searchMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = + searchMangaNextPageSelector() + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return when { + query.startsWith(PREFIX_ID_SEARCH) -> { + val id = query.removePrefix(PREFIX_ID_SEARCH).trim() + fetchMangaDetails( + SManga.create().apply { + url = "/truyen/$id" + } + ) + .map { MangasPage(listOf(it), false) } + } + else -> super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + var canAddTextFilter = true + + addPathSegment("tim-kiem") + addQueryParameter("page", page.toString()) + + if (query.isNotEmpty()) { + addQueryParameter("filter[name]", query) + canAddTextFilter = false + } + + (if (filters.isEmpty()) getFilterList() else filters).forEach { + when (it) { + is GenreList -> it.state.forEach { genre -> + when (genre.state) { + Filter.TriState.STATE_INCLUDE -> addQueryParameter("filter[accept_genres]", genre.id.toString()) + Filter.TriState.STATE_EXCLUDE -> addQueryParameter("filter[reject_genres]", genre.id.toString()) + } + } + is Author -> if (canAddTextFilter && it.state.isNotEmpty()) { + addQueryParameter("filter[artist]", it.state) + canAddTextFilter = false + } + is Doujinshi -> if (canAddTextFilter && it.state.isNotEmpty()) { + addQueryParameter("filter[doujinshi]", it.state) + canAddTextFilter = false + } + is Status -> addQueryParameter("filter[status]", it.toUriPart()) + is SortBy -> addQueryParameter("sort", it.toUriPart()) + else -> return@forEach + } + } + }.build().toString() + return GET(url, headers) + } + + override fun searchMangaSelector(): String = "div.grid div.manga-vertical" + + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.select("div.p-2.truncate a").first().attr("href")) + title = element.select("div.p-2.truncate a").first().text() + thumbnail_url = element.select("div.cover") + .first() + .attr("style") + .substringAfter("url('") + .substringBefore("')") + } + + override fun searchMangaNextPageSelector() = "li:contains(Cuối)" + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.select("span.text-lg.ml-1.font-semibold").first().text() + author = document.select("div.grow div.mt-2:contains(Tác giả) span a") + .joinToString { it.text().trim(',', ' ') } + genre = document.select("div.grow div.mt-2:contains(Thể loại) span a") + .joinToString { it.text().trim(',', ' ') } + + description = "" + document.select("div.grow div.mt-2").forEach { + val key = it.select("span.mr-2").text().trim(':', ' ') + if (key in arrayOf("Tác giả", "Thể loại", "Tình trạng", "Lần cuối")) { + return@forEach + } + val value = it.select("span:not(.mr-2)").text() + description += "$key: $value\n" + } + description += "\n" + description += document.select("p:contains(Tóm tắt) ~ p").joinToString("\n") { + it.run { + select(Evaluator.Tag("br")).prepend("\\n") + this.text().replace("\\n", "\n").replace("\n ", "\n") + } + }.trim() + + thumbnail_url = document.select(".cover") + .first() + .attr("style") + .substringAfter("url('") + .substringBefore("')") + + val statusString = document.select("div.grow div.mt-2:contains(Tình trạng) a").first().text() + status = when (statusString) { + "Đã hoàn thành" -> SManga.COMPLETED + "Đang tiến hành" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + } + + override fun chapterListSelector(): String = "ul.overflow-y-auto.overflow-x-hidden a" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.attr("href")) + name = element.select("span.text-ellipsis").text() + date_upload = kotlin.runCatching { + dateFormat.parse(element.select("span.timeago").attr("datetime"))?.time + }.getOrNull() ?: 0L + + val match = CHAPTER_NUMBER_REGEX.findAll(name) + chapter_number = if (match.count() > 1 && name.lowercase().startsWith("vol")) { + match.elementAt(1) + } else { + match.elementAtOrNull(0) + }?.value?.toFloat() ?: -1f + } + + override fun pageListParse(document: Document): List = document + .select("div.text-center img.lazy") + .mapIndexed { idx, element -> Page(idx, "", element.attr("abs:src")) } + + override fun imageUrlParse(document: Document) = throw Exception("Not used") + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + + private open class UriPartFilter(displayName: String, val vals: Array>, state: Int = 0) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { + fun toUriPart() = vals[state].second + } + + private class SortBy(state: Int = 0) : UriPartFilter( + "Sắp xếp theo", + arrayOf( + Pair("Mới cập nhật", "-updated_at"), + Pair("Mới nhất", "-created_at"), + Pair("Cũ nhất", "created_at"), + Pair("Xem nhiều", "-views"), + Pair("A-Z", "name"), + Pair("Z-A", "-name"), + ), + state + ) + + private class Status : UriPartFilter( + "Trạng thái", + arrayOf( + Pair("Tất cả", "1,2"), + Pair("Đang tiến hành", "2"), + Pair("Đã hoàn thành", "1"), + ), + ) + + private class Genre(name: String, val id: Int) : Filter.TriState(name) + private class GenreList(genres: List) : Filter.Group("Thể loại", genres) + + private class Author : Filter.Text("Tác giả", "") + private class Doujinshi : Filter.Text("Doujinshi", "") + + override fun getFilterList(): FilterList = FilterList( + SortBy(3), + GenreList(getGenreList()), + Filter.Header("Không dùng được với nhau và với tìm tựa đề"), + Author(), + Doujinshi(), + ) + + // console.log([...document.querySelectorAll("label.ml-3.inline-flex.items-center.cursor-pointer")].map(e => `Genre("${e.querySelector(".truncate").innerText}", ${e.getAttribute("@click").replace('toggleGenre(\'', '').replace('\')', '')}),`).join("\n")) + private fun getGenreList(): List = listOf( + Genre("Mature", 1), + Genre("Manhwa", 2), + Genre("Group", 3), + Genre("Housewife", 4), + Genre("NTR", 5), + Genre("Adult", 6), + Genre("Series", 7), + Genre("Complete", 8), + Genre("Ngực Lớn", 9), + Genre("Lãng Mạn", 10), + Genre("Truyện Màu", 11), + Genre("Mind Break", 12), + Genre("Mắt Kính", 13), + Genre("Ngực Nhỏ", 14), + Genre("Fantasy", 15), + Genre("Ecchi", 16), + Genre("Bạo Dâm", 17), + Genre("Harem", 18), + Genre("Hài Hước", 19), + Genre("Cosplay", 20), + Genre("Hầu Gái", 21), + Genre("Loli", 22), + Genre("Shota", 23), + Genre("Gangbang", 24), + Genre("Doujinshi", 25), + Genre("Guro", 26), + Genre("Virgin", 27), + Genre("OneShot", 28), + Genre("Chơi Hai Lỗ", 29), + Genre("Hậu Môn", 30), + Genre("Nữ Sinh", 31), + Genre("Mang Thai", 32), + Genre("Giáo Viên", 33), + Genre("Loạn Luân", 34), + Genre("Truyện Không Che", 35), + Genre("Futanari", 36), + Genre("Yuri", 37), + Genre("Nô Lệ", 38), + Genre("Đồ Bơi", 39), + Genre("Thể Thao", 40), + Genre("Truyện Ngắn", 41), + Genre("Lão Gìa Dâm", 42), + Genre("Hãm Hiếp", 43), + Genre("Monster Girl", 44), + Genre("Y Tá", 45), + Genre("Supernatural", 46), + Genre("3D", 47), + Genre("Truyện Comic", 48), + Genre("Animal girl", 49), + Genre("Thú Vật", 50), + Genre("Kinh Dị", 51), + Genre("Quái Vật", 52), + Genre("Xúc Tua", 53), + Genre("Gender Bender", 54), + Genre("Yaoi", 55), + Genre("CG", 56), + Genre("Trap", 57), + Genre("Furry", 58), + Genre("Mind Control", 59), + Genre("Elf", 60), + Genre("Côn Trùng", 61), + Genre("Kogal", 62), + Genre("Artist", 63), + Genre("Scat", 64), + Genre("Milf", 65), + Genre("LXHENTAI", 66), + ) + + companion object { + const val PREFIX_ID_SEARCH = "id:" + + val CHAPTER_NUMBER_REGEX = Regex("""[+\-]?([0-9]*[.])?[0-9]+""", RegexOption.IGNORE_CASE) + } +} diff --git a/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentaiUrlActivity.kt b/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentaiUrlActivity.kt new file mode 100644 index 000000000..aa47cbc94 --- /dev/null +++ b/src/vi/lxhentai/src/eu/kanade/tachiyomi/extension/vi/lxhentai/LxHentaiUrlActivity.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.extension.vi.lxhentai + +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 LxHentaiUrlActivity : 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", "id:$id") + putExtra("filter", packageName) + } + ) + } catch (e: ActivityNotFoundException) { + Log.e("LxHentaiUrlActivity", e.toString()) + } + } else { + Log.e("LxHentaiUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +}