diff --git a/src/en/lhtranslation/build.gradle b/src/en/lhtranslation/build.gradle new file mode 100644 index 000000000..18251d2e5 --- /dev/null +++ b/src/en/lhtranslation/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: LHTranslation' + pkgNameSuffix = 'en.lhtranslation' + extClass = '.LHTranslation' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/lhtranslation/res/mipmap-hdpi/ic_launcher.png b/src/en/lhtranslation/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..671750f95 Binary files /dev/null and b/src/en/lhtranslation/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/lhtranslation/res/mipmap-mdpi/ic_launcher.png b/src/en/lhtranslation/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..dd6f9df90 Binary files /dev/null and b/src/en/lhtranslation/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/lhtranslation/res/mipmap-xhdpi/ic_launcher.png b/src/en/lhtranslation/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..9091ca7ec Binary files /dev/null and b/src/en/lhtranslation/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/lhtranslation/res/mipmap-xxhdpi/ic_launcher.png b/src/en/lhtranslation/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..1a2199e95 Binary files /dev/null and b/src/en/lhtranslation/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/lhtranslation/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/lhtranslation/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2ad75ce41 Binary files /dev/null and b/src/en/lhtranslation/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/lhtranslation/res/web_hi_res_512.png b/src/en/lhtranslation/res/web_hi_res_512.png new file mode 100644 index 000000000..d06356e5e Binary files /dev/null and b/src/en/lhtranslation/res/web_hi_res_512.png differ diff --git a/src/en/lhtranslation/src/eu/kanade/tachiyomi/extension/en/lhtranslation/LHTranslation.kt b/src/en/lhtranslation/src/eu/kanade/tachiyomi/extension/en/lhtranslation/LHTranslation.kt new file mode 100644 index 000000000..94a883997 --- /dev/null +++ b/src/en/lhtranslation/src/eu/kanade/tachiyomi/extension/en/lhtranslation/LHTranslation.kt @@ -0,0 +1,235 @@ +package eu.kanade.tachiyomi.extension.en.lhtranslation + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.util.* + +class LHTranslation : ParsedHttpSource() { + // Ps. LHTranslation is really similar to rawLH + override val name = "LHTranslation" + + override val baseUrl = "https://lhtranslation.net" + + override val lang = "en" + + override val supportsLatest = true + + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/manga-list.html?listType=pagination&page=$page&artist=&author=&group=&m_status=&name=&genre=&ungenre=&sort=views&sort_type=DESC", headers) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/manga-list.html?")!!.newBuilder().addQueryParameter("name", query) + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is Status -> { + val status = arrayOf("", "1", "2")[filter.state] + url.addQueryParameter("m_status", status) + } + is TextField -> url.addQueryParameter(filter.key, filter.state) + is GenreList -> { + + var genre = String() + var ungenre = String() + + filter.state.forEach { + if (it.isIncluded()) genre += ",${it.name}" + if (it.isExcluded()) ungenre += ",${it.name}" + } + url.addQueryParameter("genre", genre) + url.addQueryParameter("ungenre", ungenre) + } + } + } + return GET(url.toString(), headers) + } + + override fun latestUpdatesRequest(page: Int): Request = + GET("$baseUrl/manga-list.html?listType=pagination&page=$page&artist=&author=&group=&m_status=&name=&genre=&sort=last_update&sort_type=DESC") + + override fun popularMangaSelector() = "div.media" + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun searchMangaSelector() = popularMangaSelector() + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("h3 > a").first().let { + manga.setUrlWithoutDomain("/" + it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga = + popularMangaFromElement(element) + + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun popularMangaNextPageSelector() = "a:contains(ยป)" + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + override fun mangaDetailsParse(document: Document): SManga { + val manga = SManga.create() + val infoElement = document.select("div.row").first() + val genres = infoElement.select("ul.manga-info li:nth-child(5) small a")?.map { + it.text() + } + manga.author = infoElement.select("small a.btn.btn-xs.btn-info").first()?.text() + manga.genre = genres?.joinToString(", ") + manga.status = parseStatus(infoElement.select("a.btn.btn-xs.btn-success").first().text()) + + manga.description = document.select("div.row > p")?.text()?.trim() + val imgUrl = document.select("img.thumbnail").first()?.attr("src") + if (imgUrl!!.startsWith("app/")) { + manga.thumbnail_url = "$baseUrl/$imgUrl" + } else { + manga.thumbnail_url = imgUrl + } + return manga + } + + private fun parseStatus(element: String): Int = when { + element.contains("Completed") -> SManga.COMPLETED + element.contains("Ongoing") -> SManga.ONGOING + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = ".list-chapters .list-wrap p" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select(".titleLink a").first() + val timeElement = element.select(".pubDate time").first() + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain("/" + urlElement.attr("href")) + chapter.name = urlElement.text() + chapter.date_upload = parseChapterDate(timeElement.text()) + return chapter + } + + private fun parseChapterDate(date: String): Long { + val value = date.split(' ')[0].toInt() + return when { + "minute(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + set(Calendar.MINUTE, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + "hour(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.HOUR_OF_DAY, value * -1) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + "day(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * -1) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + "week(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.DATE, value * 7 * -1) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + "month(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.MONTH, value * -1) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + "year(s) ago" in date -> Calendar.getInstance().apply { + add(Calendar.YEAR, value * -1) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + else -> { + return 0 + } + } + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + document.select(".chapter-img").forEach { + val url = it.attr("src") + if (url != "") { + pages.add(Page(pages.size, "", url)) + } + } + return pages + } + + override fun imageUrlParse(document: Document) = "" + + override fun imageRequest(page: Page): Request { + val imgHeader = Headers.Builder().apply { + add("Referer", baseUrl) + }.build() + return GET(page.imageUrl!!, imgHeader) + } + + private class TextField(name: String, val key: String) : Filter.Text(name) + private class Status : Filter.Select("Status", arrayOf("Any", "Completed", "Ongoing")) + private class GenreList(genres: List) : Filter.Group("Genre", genres) + private class Genre(name: String, val id: String = name.replace(' ', '+')) : Filter.TriState(name) + + // TODO: Country + override fun getFilterList() = FilterList( + TextField("Author", "author"), + TextField("Group", "group"), + Status(), + GenreList(getGenreList()) + ) + + // [...document.querySelectorAll("div.panel-body a")].map((el,i) => `Genre("${el.innerText.trim()}")`).join(',\n') + // on https://lhtranslation.net/search + private fun getGenreList() = listOf( + Genre("Action"), + Genre("18+"), + Genre("Adult"), + Genre("Anime"), + Genre("Comedy"), + Genre("Comic"), + Genre("Doujinshi"), + Genre("Drama"), + Genre("Ecchi"), + Genre("Fantasy"), + Genre("Gender Bender"), + Genre("Harem"), + Genre("Historical"), + Genre("Horror"), + Genre("Josei"), + Genre("Live action"), + Genre("Manhua"), + Genre("Manhwa"), + Genre("Martial Art"), + Genre("Mature"), + Genre("Mecha"), + Genre("Mystery"), + Genre("One shot"), + Genre("Psychological"), + Genre("Romance"), + Genre("School Life"), + Genre("Sci-fi"), + Genre("Seinen"), + Genre("Shoujo"), + Genre("Shojou Ai"), + Genre("Shounen"), + Genre("Shounen Ai"), + Genre("Slice of Life"), + Genre("Smut"), + Genre("Sports"), + Genre("Supernatural"), + Genre("Tragedy"), + Genre("Adventure"), + Genre("Yaoi") + ) +}