From 9e00ce42fee6122e85f983716ff884b05b34dc13 Mon Sep 17 00:00:00 2001
From: e-shl <35057681+e-shl@users.noreply.github.com>
Date: Sun, 19 Dec 2021 17:05:21 +0500
Subject: [PATCH] New [RU] Hentailib (#10135)
* New [RU] Hentailib
* -mangalib
* option change language in latest
* icon
* icon microperfect
---
src/ru/libhentai/AndroidManifest.xml | 25 +
src/ru/libhentai/build.gradle | 17 +
.../libhentai/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1886 bytes
.../libhentai/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1578 bytes
.../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2549 bytes
.../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3670 bytes
.../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5052 bytes
src/ru/libhentai/res/web_hi_res_512.png | Bin 0 -> 10925 bytes
.../extension/ru/libhentai/LibHentai.kt | 889 ++++++++++++++++++
.../ru/libhentai/LibHentaiActivity.kt | 41 +
10 files changed, 972 insertions(+)
create mode 100644 src/ru/libhentai/AndroidManifest.xml
create mode 100644 src/ru/libhentai/build.gradle
create mode 100644 src/ru/libhentai/res/mipmap-hdpi/ic_launcher.png
create mode 100644 src/ru/libhentai/res/mipmap-mdpi/ic_launcher.png
create mode 100644 src/ru/libhentai/res/mipmap-xhdpi/ic_launcher.png
create mode 100644 src/ru/libhentai/res/mipmap-xxhdpi/ic_launcher.png
create mode 100644 src/ru/libhentai/res/mipmap-xxxhdpi/ic_launcher.png
create mode 100644 src/ru/libhentai/res/web_hi_res_512.png
create mode 100644 src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentai.kt
create mode 100644 src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentaiActivity.kt
diff --git a/src/ru/libhentai/AndroidManifest.xml b/src/ru/libhentai/AndroidManifest.xml
new file mode 100644
index 000000000..c33cfb934
--- /dev/null
+++ b/src/ru/libhentai/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ru/libhentai/build.gradle b/src/ru/libhentai/build.gradle
new file mode 100644
index 000000000..78fa3a4e6
--- /dev/null
+++ b/src/ru/libhentai/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'HentaiLib'
+ pkgNameSuffix = 'ru.libhentai'
+ extClass = '.LibHentai'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+dependencies {
+ implementation project(path: ':lib-ratelimit')
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/ru/libhentai/res/mipmap-hdpi/ic_launcher.png b/src/ru/libhentai/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..41cb9dd7a2abd57e448c2a5f3232d5cd05bbde10
GIT binary patch
literal 1886
zcmV-k2ch_hP)|O6;cD*)E0w7>sT{_ybUCyyVSbx1lmFz?Z$$CmByA6WE=
zx`z+by9|uiKj{F
zSc;U2$s}1!wu(05g^L0523QniM6|N9!hwV3HUa=w>h^7%$<
zYsTer=xr{CYI4>V5XPYaGQ+`DtF_+%;EG+KQ2@BMv$OLQFDn%g4UQR56Q=>BIGR1}
zKu%^4ZhgJ_6acP)&1E(Q0G?WH`v~7}2#^_$&DqlpP`AqqS1eYJ0Kn6{W?MklJAi0$
zC}+>x)l~wd!s$QI_3nVg6jG#2_c*jbHkKC+kP*(5pK$@GxrLhoImB@a7y#dmmsW4D7LmmKWfoO5u0Mf&84T#|-BuA6_IL$++T_8I+fuKMIWOw#-4OFex
zj_`e@1E>k31q!h-RI>sS61H%<1v0|9=?%FD#0wIt$yrpB72VoTX(U{XhU&&7PhB
znZe+#dk}&2a9ja01u-~{4+4-8PJe+kFfE{+z5}h7Hf=4f-T@h)v_JR_5|9~=J3uBN
zM#lv+AiT|+ZtE{kRK0k4f{u@0nQrqKXv)tZVTMBmA&v@sOjVu}QJxc#*XI&YrD6`G
zg`ojDJv~DVFLk_A*1lKPzF!V9qUnR;dTEo)nj9ZP|1B@8TUsKq0j76AMkwu1#nlaK
zAUil1=*^o`!=GDRuNeci;kXWD1)@DF&;W(GxG7FM3~nYIr3d1~!2r_2;40N^Y9JaM
zZ-Au+n)HR$KpLE$0vUm551BMTR!d6z@{0xt?^Xmz2gh|FGZekkHw{pTjalMopNee*
z8Q}B>O+VeA6k
z^oA}LNDo9?xnd8<3db8@(R9cf2w!wxfoMRqTec`Es9f1nRVvp}sa!{^Yh|*$XkhU1
z!5d)FcdP5DxL(tZizEjVNN>Bg+PdxHn7z8tJA_^+kQIm-iX9lIIDH4A0qGTtFvFo7
zA8W*ks#e*kVZX+cg9BuSVi!g`YYIZV^Hgr~giKq{fUGc_qUbHBDL;b>7OyP^0*DsH
zO&EN?=>T#8r>{UxK&)`uZ@>h|C77NAbqhi>YdU~j#&H#>SS%g6g<=PW{9D-KRDRJw
zo~5OgA9{si1f%+|XCHtbJXrk&9DE#V6aYS(oSft?0tV;xg%BHHTyL`Ncig}KU>5*B
zw+l1^jgKjQ?e{Z()7*c|K;7gBZBJK?^&wbM@@=deI
z7ii`6GnP&H!-h@zLKjfu_j-N7e>|QE2>>Pl;1W3a4E-noTm^s|05CH*H+OjF&fTNs
z<@J$C^K20vc1%2LJ&8nEpTu0D!l7EfdXa
z8-p&sfdPMC$0ca2Qx7z@#4kS3u0!L7d=&sbf!6nUbUiRszrT#6SGfi=9K$gj!_mv}
YKXUD{nfr<4-2eap07*qoM6N<$f;uu$vH$=8
literal 0
HcmV?d00001
diff --git a/src/ru/libhentai/res/mipmap-mdpi/ic_launcher.png b/src/ru/libhentai/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..40d1a9c7844e3a332f21ea6202a7527c3aec431f
GIT binary patch
literal 1578
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
LNkvXXu0mjfN_n$n
literal 0
HcmV?d00001
diff --git a/src/ru/libhentai/res/mipmap-xxhdpi/ic_launcher.png b/src/ru/libhentai/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..01ba951d014e4a838e80248cd40785b71acb2f38
GIT binary patch
literal 3670
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
EKSv;pMgRZ+
literal 0
HcmV?d00001
diff --git a/src/ru/libhentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/ru/libhentai/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..8171958cd35ac6f9ee62cab100454dfa4125c3ab
GIT binary patch
literal 5052
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
literal 0
HcmV?d00001
diff --git a/src/ru/libhentai/res/web_hi_res_512.png b/src/ru/libhentai/res/web_hi_res_512.png
new file mode 100644
index 0000000000000000000000000000000000000000..ace364704002a01c29b3553e202ad4a4c95e9ca6
GIT binary patch
literal 10925
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
OPd;8W_uNO)fBzqCRPF8n
literal 0
HcmV?d00001
diff --git a/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentai.kt b/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentai.kt
new file mode 100644
index 000000000..5c9ef8e0b
--- /dev/null
+++ b/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentai.kt
@@ -0,0 +1,889 @@
+package eu.kanade.tachiyomi.extension.ru.libhentai
+
+import android.app.Application
+import android.content.SharedPreferences
+import android.widget.Toast
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceScreen
+import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.source.ConfigurableSource
+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.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.contentOrNull
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.intOrNull
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+
+class LibHentai : ConfigurableSource, HttpSource() {
+
+ private val json: Json by injectLazy()
+
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_${id}_2", 0x0000)
+ }
+
+ override val name: String = "Hentailib"
+
+ override val lang = "ru"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient.newBuilder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .addNetworkInterceptor(RateLimitInterceptor(3))
+ .build()
+
+ override val baseUrl = "https://hentailib.me"
+
+ override fun headersBuilder() = Headers.Builder().apply {
+ add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
+ add("Accept", "image/webp,*/*;q=0.8")
+ add("Referer", baseUrl)
+ }
+
+ override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
+
+ private val latestUpdatesSelector = "div.updates__item"
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val elements = response.asJsoup().select(latestUpdatesSelector)
+ val latestMangas = elements?.map { latestUpdatesFromElement(it) }
+ if (latestMangas != null)
+ return MangasPage(latestMangas, false) // TODO: use API
+ return MangasPage(emptyList(), false)
+ }
+
+ private fun latestUpdatesFromElement(element: Element): SManga {
+ val manga = SManga.create()
+ element.select("div.cover").first().let { img ->
+ manga.thumbnail_url = img.attr("data-src").replace("_thumb", "_250x350")
+ }
+
+ element.select("a").first().let { link ->
+ manga.setUrlWithoutDomain(link.attr("href"))
+ manga.title = if (titleLanguage.equals("rus") || element.select(".updates__name_rus").isNullOrEmpty()) { element.select("h4").first().text() } else element.select(".updates__name_rus").first().text()
+ }
+ return manga
+ }
+
+ private var csrfToken: String = ""
+
+ private fun catalogHeaders() = Headers.Builder()
+ .apply {
+ add("Accept", "application/json, text/plain, */*")
+ add("X-Requested-With", "XMLHttpRequest")
+ add("x-csrf-token", csrfToken)
+ }
+ .build()
+
+ override fun popularMangaRequest(page: Int) = GET("$baseUrl/login", headers)
+
+ override fun fetchPopularManga(page: Int): Observable {
+ if (csrfToken.isEmpty()) {
+ return client.newCall(popularMangaRequest(page))
+ .asObservableSuccess()
+ .flatMap { response ->
+ // Obtain token
+ val resBody = response.body!!.string()
+ csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
+ return@flatMap fetchPopularMangaFromApi(page)
+ }
+ }
+ return fetchPopularMangaFromApi(page)
+ }
+
+ private fun fetchPopularMangaFromApi(page: Int): Observable {
+ return client.newCall(POST("$baseUrl/filterlist?dir=desc&sort=views&page=$page", catalogHeaders()))
+ .asObservableSuccess()
+ .map { response ->
+ popularMangaParse(response)
+ }
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val resBody = response.body!!.string()
+ val result = json.decodeFromString(resBody)
+ val items = result["items"]!!.jsonObject
+ val popularMangas = items["data"]?.jsonArray?.map { popularMangaFromElement(it) }
+
+ if (popularMangas != null) {
+ val hasNextPage = items["next_page_url"]?.jsonPrimitive?.contentOrNull != null
+ return MangasPage(popularMangas, hasNextPage)
+ }
+ return MangasPage(emptyList(), false)
+ }
+
+ private fun popularMangaFromElement(el: JsonElement) = SManga.create().apply {
+ val slug = el.jsonObject["slug"]!!.jsonPrimitive.content
+ val cover = el.jsonObject["cover"]!!.jsonPrimitive.content
+ title = if (titleLanguage.equals("rus")) el.jsonObject["rus_name"]!!.jsonPrimitive.content else el.jsonObject["name"]!!.jsonPrimitive.content
+ thumbnail_url = "$COVER_URL/huploads/cover/$slug/cover/${cover}_250x350.jpg"
+ url = "/$slug"
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val document = response.asJsoup()
+
+ if (document.select("body[data-page=home]").isNotEmpty())
+ throw Exception("Can't open manga. Try log in via WebView")
+
+ val manga = SManga.create()
+
+ val body = document.select("div.media-info-list").first()
+ val rawCategory = body.select("div.media-info-list__title:contains(Тип) + div").text()
+ val category = when {
+ rawCategory == "Комикс западный" -> "Комикс"
+ rawCategory.isNotBlank() -> rawCategory
+ else -> "Манга"
+ }
+ var rawAgeStop = body.select("div.media-info-list__title:contains(Возрастной рейтинг) + div").text()
+ if (rawAgeStop.isEmpty()) {
+ rawAgeStop = "0+"
+ }
+
+ val ratingValue = document.select(".media-rating.media-rating_lg div.media-rating__value").text().toFloat() * 2
+ val ratingVotes = document.select(".media-rating.media-rating_lg div.media-rating__votes").text()
+ val ratingStar = when {
+ ratingValue > 9.5 -> "★★★★★"
+ ratingValue > 8.5 -> "★★★★✬"
+ ratingValue > 7.5 -> "★★★★☆"
+ ratingValue > 6.5 -> "★★★✬☆"
+ ratingValue > 5.5 -> "★★★☆☆"
+ ratingValue > 4.5 -> "★★✬☆☆"
+ ratingValue > 3.5 -> "★★☆☆☆"
+ ratingValue > 2.5 -> "★✬☆☆☆"
+ ratingValue > 1.5 -> "★☆☆☆☆"
+ ratingValue > 0.5 -> "✬☆☆☆☆"
+ else -> "☆☆☆☆☆"
+ }
+ val genres = document.select(".media-tags > a").map { it.text().capitalize() }
+ manga.title = if (titleLanguage.equals("rus")) document.select(".media-name__main").text() else document.select(".media-name__alt").text()
+ manga.thumbnail_url = document.select(".media-sidebar__cover > img").attr("src")
+ manga.author = body.select("div.media-info-list__title:contains(Автор) + div").text()
+ manga.artist = body.select("div.media-info-list__title:contains(Художник) + div").text()
+ manga.status = if (document.html().contains("Манга удалена по просьбе правообладателей") ||
+ document.html().contains("Данный тайтл лицензирован на территории РФ.")
+ ) {
+ SManga.LICENSED
+ } else
+ when (
+ body.select("div.media-info-list__title:contains(Статус перевода) + div")
+ .text()
+ .toLowerCase(Locale.ROOT)
+ ) {
+ "продолжается" -> SManga.ONGOING
+ "завершен" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ manga.genre = genres.plusElement(category).plusElement(rawAgeStop).joinToString { it.trim() }
+ val altSelector = document.select(".media-info-list__item_alt-names .media-info-list__value div")
+ var altName = ""
+ if (altSelector.isNotEmpty()) {
+ altName = "Альтернативные названия:\n" + altSelector.map { it.text() }.joinToString(" / ") + "\n\n"
+ }
+ val mediaNameLanguage = if (titleLanguage.equals("rus")) document.select(".media-name__alt").text() else document.select(".media-name__main").text()
+ manga.description = mediaNameLanguage + "\n" + ratingStar + " " + ratingValue + " (голосов: " + ratingVotes + ")\n" + altName + document.select(".media-description__text").text()
+ return manga
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+ if (document.html().contains("Манга удалена по просьбе правообладателей") ||
+ document.html().contains("Данный тайтл лицензирован на территории РФ.")
+ ) {
+ return emptyList()
+ }
+ val dataStr = document
+ .toString()
+ .substringAfter("window.__DATA__ = ")
+ .substringBefore("window._SITE_COLOR_")
+ .substringBeforeLast(";")
+
+ val data = json.decodeFromString(dataStr)
+ val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
+ val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
+ val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
+ val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
+
+ val chapters: List? = if (branches.isNotEmpty() && !sortingList.equals("ms_mixing")) {
+ sortChaptersByTranslator(sortingList, chaptersList, slug, branches)
+ } else {
+ chaptersList
+ ?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
+ ?.map { chapterFromElement(it, sortingList, slug) }
+ }
+
+ return chapters ?: emptyList()
+ }
+
+ private fun sortChaptersByTranslator
+ (sortingList: String?, chaptersList: JsonArray?, slug: String, branches: List): List? {
+ var chapters: List? = null
+ when (sortingList) {
+ "ms_combining" -> {
+ val tempChaptersList = mutableListOf()
+ for (currentBranch in branches.withIndex()) {
+ val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
+ chapters = chaptersList
+ ?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
+ ?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
+ chapters?.let { tempChaptersList.addAll(it) }
+ }
+ chapters = tempChaptersList
+ }
+ "ms_largest" -> {
+ val sizesChaptersLists = mutableListOf()
+ for (currentBranch in branches.withIndex()) {
+ val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
+ val chapterSize = chaptersList
+ ?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId }!!.size
+ sizesChaptersLists.add(chapterSize)
+ }
+ val max = sizesChaptersLists.indexOfFirst { it == sizesChaptersLists.maxOrNull() ?: 0 }
+ val teamId = branches[max].jsonObject["id"]!!.jsonPrimitive.int
+
+ chapters = chaptersList
+ ?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
+ ?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
+ }
+ "ms_active" -> {
+ for (currentBranch in branches.withIndex()) {
+ val teams = branches[currentBranch.index].jsonObject["teams"]!!.jsonArray
+ for (currentTeam in teams.withIndex()) {
+ if (teams[currentTeam.index].jsonObject["is_active"]!!.jsonPrimitive.int == 1) {
+ val teamId = branches[currentBranch.index].jsonObject["id"]!!.jsonPrimitive.int
+ chapters = chaptersList
+ ?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
+ ?.map { chapterFromElement(it, sortingList, slug, teamId, branches) }
+ break
+ }
+ }
+ }
+ chapters ?: throw Exception("Активный перевод не назначен на сайте")
+ }
+ }
+
+ return chapters
+ }
+
+ private fun chapterFromElement
+ (chapterItem: JsonElement, sortingList: String?, slug: String, teamIdParam: Int? = null, branches: List? = null): SChapter {
+ val chapter = SChapter.create()
+
+ val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
+ val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
+ val teamId = if (teamIdParam != null) "?bid=$teamIdParam" else ""
+
+ val url = "$baseUrl/$slug/v$volume/c$number$teamId"
+
+ chapter.setUrlWithoutDomain(url)
+
+ val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
+ val fullNameChapter = "Том $volume. Глава $number"
+
+ if (!sortingList.equals("ms_mixing")) {
+ chapter.scanlator = branches?.let { getScanlatorTeamName(it, chapterItem) } ?: chapterItem.jsonObject["username"]!!.jsonPrimitive.content
+ }
+ chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
+ chapter.date_upload = SimpleDateFormat("yyyy-MM-dd", Locale.US)
+ .parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
+
+ return chapter
+ }
+
+ private fun getScanlatorTeamName(branches: List, chapterItem: JsonElement): String? {
+ var scanlatorData: String? = null
+ for (currentBranch in branches.withIndex()) {
+ val branch = branches[currentBranch.index].jsonObject
+ val teams = branch["teams"]!!.jsonArray
+ if (chapterItem.jsonObject["branch_id"]!!.jsonPrimitive.int == branch["id"]!!.jsonPrimitive.int) {
+ for (currentTeam in teams.withIndex()) {
+ val team = teams[currentTeam.index].jsonObject
+ val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
+ scanlatorData = if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
+ (scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
+ ) team["name"]!!.jsonPrimitive.content else branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
+ }
+ }
+ }
+ return scanlatorData
+ }
+
+ override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
+ """Глава\s(\d+)""".toRegex().find(chapter.name)?.let {
+ val number = it.groups[1]?.value!!
+ chapter.chapter_number = number.toFloat()
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ val document = response.asJsoup()
+
+ val redirect = document.html()
+ if (!redirect.contains("window.__info")) {
+ if (redirect.contains("hold-transition login-page")) {
+ throw Exception("Для просмотра 18+ контента необходима авторизация через WebView")
+ } else if (redirect.contains("header__logo")) {
+ throw Exception("Лицензировано - Главы не доступны")
+ }
+ }
+
+ val chapInfo = document
+ .select("script:containsData(window.__info)")
+ .first()
+ .html()
+ .split("window.__info = ")
+ .last()
+ .trim()
+ .split(";")
+ .first()
+
+ val chapInfoJson = json.decodeFromString(chapInfo)
+ val servers = chapInfoJson["servers"]!!.jsonObject.toMap()
+ val defaultServer: String = chapInfoJson["img"]!!.jsonObject["server"]!!.jsonPrimitive.content
+ val autoServer = setOf("secondary", "fourth", defaultServer, "compress")
+ val imgUrl: String = chapInfoJson["img"]!!.jsonObject["url"]!!.jsonPrimitive.content
+
+ val serverToUse = when (this.server) {
+ null -> autoServer
+ "auto" -> autoServer
+ else -> listOf(this.server)
+ }
+
+ // Get pages
+ val pagesArr = document
+ .select("script:containsData(window.__pg)")
+ .first()
+ .html()
+ .trim()
+ .removePrefix("window.__pg = ")
+ .removeSuffix(";")
+
+ val pagesJson = json.decodeFromString(pagesArr)
+ val pages = mutableListOf()
+
+ pagesJson.forEach { page ->
+ val keys = servers.keys.filter { serverToUse.indexOf(it) >= 0 }.sortedBy { serverToUse.indexOf(it) }
+ val serversUrls = keys.map {
+ servers[it]?.jsonPrimitive?.contentOrNull + imgUrl + page.jsonObject["u"]!!.jsonPrimitive.content
+ }.joinToString(separator = ",,") { it }
+ pages.add(Page(page.jsonObject["p"]!!.jsonPrimitive.int, serversUrls))
+ }
+
+ return pages
+ }
+
+ private fun checkImage(url: String): Boolean {
+ val response = client.newCall(Request.Builder().url(url).head().headers(headers).build()).execute()
+ return response.isSuccessful && (response.header("content-length", "0")?.toInt()!! > 320)
+ }
+
+ override fun fetchImageUrl(page: Page): Observable {
+ if (page.imageUrl != null) {
+ return Observable.just(page.imageUrl)
+ }
+
+ val urls = page.url.split(",,")
+ if (urls.size == 1) {
+ return Observable.just(urls[0])
+ }
+
+ return Observable.from(urls).filter { checkImage(it) }.first()
+ }
+
+ override fun imageUrlParse(response: Response): String = ""
+
+ private fun searchMangaByIdRequest(id: String): Request {
+ return GET("$baseUrl/$id", headers)
+ }
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
+ val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH)
+ client.newCall(searchMangaByIdRequest(realQuery))
+ .asObservableSuccess()
+ .map { response ->
+ val details = mangaDetailsParse(response)
+ details.url = "/$realQuery"
+ MangasPage(listOf(details), false)
+ }
+ } else {
+ client.newCall(searchMangaRequest(page, query, filters))
+ .asObservableSuccess()
+ .map { response ->
+ searchMangaParse(response)
+ }
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ if (csrfToken.isEmpty()) {
+ val tokenResponse = client.newCall(popularMangaRequest(page)).execute()
+ val resBody = tokenResponse.body!!.string()
+ csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
+ }
+ val url = "$baseUrl/filterlist?page=$page".toHttpUrlOrNull()!!.newBuilder()
+ if (query.isNotEmpty()) {
+ url.addQueryParameter("name", query)
+ }
+ (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
+ when (filter) {
+ is CategoryList -> filter.state.forEach { category ->
+ if (category.state) {
+ url.addQueryParameter("types[]", category.id)
+ }
+ }
+ is FormatList -> filter.state.forEach { forma ->
+ if (forma.state != Filter.TriState.STATE_IGNORE) {
+ url.addQueryParameter(if (forma.isIncluded()) "format[include][]" else "format[exclude][]", forma.id)
+ }
+ }
+ is StatusList -> filter.state.forEach { status ->
+ if (status.state) {
+ url.addQueryParameter("status[]", status.id)
+ }
+ }
+ is StatusTitleList -> filter.state.forEach { title ->
+ if (title.state) {
+ url.addQueryParameter("manga_status[]", title.id)
+ }
+ }
+ is GenreList -> filter.state.forEach { genre ->
+ if (genre.state != Filter.TriState.STATE_IGNORE) {
+ url.addQueryParameter(if (genre.isIncluded()) "genres[include][]" else "genres[exclude][]", genre.id)
+ }
+ }
+ is OrderBy -> {
+ url.addQueryParameter("dir", if (filter.state!!.ascending) "asc" else "desc")
+ url.addQueryParameter("sort", arrayOf("rate", "name", "views", "created_at", "last_chapter_at", "chap_count")[filter.state!!.index])
+ }
+ is TagList -> filter.state.forEach { tag ->
+ if (tag.state != Filter.TriState.STATE_IGNORE) {
+ url.addQueryParameter(if (tag.isIncluded()) "tags[include][]" else "tags[exclude][]", tag.id)
+ }
+ }
+ }
+ }
+ return POST(url.toString(), catalogHeaders())
+ }
+
+ // Hack search method to add some results from search popup
+ override fun searchMangaParse(response: Response): MangasPage {
+ val searchRequest = response.request.url.queryParameter("name")
+ val mangas = mutableListOf()
+
+ if (!searchRequest.isNullOrEmpty()) {
+ val popupSearchHeaders = headers
+ .newBuilder()
+ .add("Accept", "application/json, text/plain, */*")
+ .add("X-Requested-With", "XMLHttpRequest")
+ .build()
+
+ // +200ms
+ val popup = client.newCall(
+ GET("$baseUrl/search?query=$searchRequest", popupSearchHeaders)
+ )
+ .execute().body!!.string()
+
+ val jsonList = json.decodeFromString(popup)
+ jsonList.forEach {
+ mangas.add(popularMangaFromElement(it))
+ }
+ }
+ val searchedMangas = popularMangaParse(response)
+
+ // Filtered out what find in popup search
+ mangas.addAll(
+ searchedMangas.mangas.filter { search ->
+ mangas.find { search.title == it.title } == null
+ }
+ )
+
+ return MangasPage(mangas, searchedMangas.hasNextPage)
+ }
+
+ private class SearchFilter(name: String, val id: String) : Filter.TriState(name)
+ private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
+
+ private class CategoryList(categories: List) : Filter.Group("Тип", categories)
+ private class FormatList(formas: List) : Filter.Group("Формат выпуска", formas)
+ private class StatusList(statuses: List) : Filter.Group("Статус перевода", statuses)
+ private class StatusTitleList(titles: List) : Filter.Group("Статус тайтла", titles)
+ private class GenreList(genres: List) : Filter.Group("Жанры", genres)
+ private class TagList(tags: List) : Filter.Group("Теги", tags)
+ private class AgeList(ages: List) : Filter.Group("Возрастное ограничение", ages)
+
+ override fun getFilterList() = FilterList(
+ OrderBy(),
+ CategoryList(getCategoryList()),
+ FormatList(getFormatList()),
+ GenreList(getGenreList()),
+ TagList(getTagList()),
+ StatusList(getStatusList()),
+ StatusTitleList(getStatusTitleList())
+ )
+
+ private class OrderBy : Filter.Sort(
+ "Сортировка",
+ arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
+ Selection(0, false)
+ )
+
+ /*
+ * Use console
+ * Object.entries(__FILTER_ITEMS__.types).map(([k, v]) => `SearchFilter("${v.label}", "${v.id}")`).join(',\n')
+ * on /manga-list
+ */
+ private fun getCategoryList() = listOf(
+ CheckFilter("Манга", "1"),
+ CheckFilter("OEL-манга", "4"),
+ CheckFilter("Манхва", "5"),
+ CheckFilter("Маньхуа", "6"),
+ CheckFilter("Руманга", "8"),
+ CheckFilter("Комикс западный", "9")
+ )
+
+ private fun getFormatList() = listOf(
+ SearchFilter("4-кома (Ёнкома)", "1"),
+ SearchFilter("Сборник", "2"),
+ SearchFilter("Додзинси", "3"),
+ SearchFilter("Сингл", "4"),
+ SearchFilter("В цвете", "5"),
+ SearchFilter("Веб", "6")
+ )
+
+ /*
+ * Use console
+ * Object.entries(__FILTER_ITEMS__.status).map(([k, v]) => `SearchFilter("${v.label}", "${v.id}")`).join(',\n')
+ * on /manga-list
+ */
+ private fun getStatusList() = listOf(
+ CheckFilter("Продолжается", "1"),
+ CheckFilter("Завершен", "2"),
+ CheckFilter("Заморожен", "3"),
+ CheckFilter("Заброшен", "4")
+ )
+
+ private fun getStatusTitleList() = listOf(
+ CheckFilter("Онгоинг", "1"),
+ CheckFilter("Завершён", "2"),
+ CheckFilter("Анонс", "3"),
+ CheckFilter("Приостановлен", "4"),
+ CheckFilter("Выпуск прекращён", "5"),
+ )
+
+ /*
+ * Use console
+ * __FILTER_ITEMS__.genres.map(it => `SearchFilter("${it.name}", "${it.id}")`).join(',\n')
+ * on /manga-list
+ */
+ private fun getGenreList() = listOf(
+ SearchFilter("арт", "32"),
+ SearchFilter("боевик", "34"),
+ SearchFilter("боевые искусства", "35"),
+ SearchFilter("вампиры", "36"),
+ SearchFilter("гарем", "37"),
+ SearchFilter("гендерная интрига", "38"),
+ SearchFilter("героическое фэнтези", "39"),
+ SearchFilter("детектив", "40"),
+ SearchFilter("дзёсэй", "41"),
+ SearchFilter("драма", "43"),
+ SearchFilter("игра", "44"),
+ SearchFilter("исекай", "79"),
+ SearchFilter("история", "45"),
+ SearchFilter("киберпанк", "46"),
+ SearchFilter("кодомо", "76"),
+ SearchFilter("комедия", "47"),
+ SearchFilter("махо-сёдзё", "48"),
+ SearchFilter("меха", "49"),
+ SearchFilter("мистика", "50"),
+ SearchFilter("научная фантастика", "51"),
+ SearchFilter("омегаверс", "77"),
+ SearchFilter("повседневность", "52"),
+ SearchFilter("постапокалиптика", "53"),
+ SearchFilter("приключения", "54"),
+ SearchFilter("психология", "55"),
+ SearchFilter("романтика", "56"),
+ SearchFilter("самурайский боевик", "57"),
+ SearchFilter("сверхъестественное", "58"),
+ SearchFilter("сёдзё", "59"),
+ SearchFilter("сёдзё-ай", "60"),
+ SearchFilter("сёнэн", "61"),
+ SearchFilter("сёнэн-ай", "62"),
+ SearchFilter("спорт", "63"),
+ SearchFilter("сэйнэн", "64"),
+ SearchFilter("трагедия", "65"),
+ SearchFilter("триллер", "66"),
+ SearchFilter("ужасы", "67"),
+ SearchFilter("фантастика", "68"),
+ SearchFilter("фэнтези", "69"),
+ SearchFilter("школа", "70"),
+ SearchFilter("эротика", "71"),
+ SearchFilter("этти", "72"),
+ SearchFilter("юри", "73"),
+ SearchFilter("яой", "74")
+ )
+
+ private fun getTagList() = listOf(
+ SearchFilter("3D", "1"),
+ SearchFilter("Defloration", "287"),
+ SearchFilter("FPP(Вид от первого лица)", "289"),
+ SearchFilter("Footfuck", "5"),
+ SearchFilter("Handjob", "6"),
+ SearchFilter("Lactation", "7"),
+ SearchFilter("Living clothes", "284"),
+ SearchFilter("Mind break", "9"),
+ SearchFilter("Scat", "13"),
+ SearchFilter("Selfcest", "286"),
+ SearchFilter("Shemale", "220"),
+ SearchFilter("Tomboy", "14"),
+ SearchFilter("Unbirth", "283"),
+ SearchFilter("X-Ray", "15"),
+ SearchFilter("Алкоголь", "16"),
+ SearchFilter("Анал", "17"),
+ SearchFilter("Андроид", "18"),
+ SearchFilter("Анилингус", "19"),
+ SearchFilter("Анимация (GIF)", "350"),
+ SearchFilter("Арт", "20"),
+ SearchFilter("Ахэгао", "2"),
+ SearchFilter("БДСМ", "22"),
+ SearchFilter("Бакуню", "21"),
+ SearchFilter("Бара", "293"),
+ SearchFilter("Без проникновения", "336"),
+ SearchFilter("Без текста", "23"),
+ SearchFilter("Без трусиков", "24"),
+ SearchFilter("Без цензуры", "25"),
+ SearchFilter("Беременность", "26"),
+ SearchFilter("Бикини", "27"),
+ SearchFilter("Близнецы", "28"),
+ SearchFilter("Боди-арт", "29"),
+ SearchFilter("Больница", "30"),
+ SearchFilter("Большая грудь", "31"),
+ SearchFilter("Большая попка", "32"),
+ SearchFilter("Борьба", "33"),
+ SearchFilter("Буккакэ", "34"),
+ SearchFilter("В бассейне", "35"),
+ SearchFilter("В ванной", "36"),
+ SearchFilter("В государственном учреждении", "37"),
+ SearchFilter("В общественном месте", "38"),
+ SearchFilter("В очках", "8"),
+ SearchFilter("В первый раз", "39"),
+ SearchFilter("В транспорте", "40"),
+ SearchFilter("Вампиры", "41"),
+ SearchFilter("Вибратор", "42"),
+ SearchFilter("Втроём", "43"),
+ SearchFilter("Гипноз", "44"),
+ SearchFilter("Глубокий минет", "45"),
+ SearchFilter("Горячий источник", "46"),
+ SearchFilter("Групповой секс", "47"),
+ SearchFilter("Гуро", "307"),
+ SearchFilter("Гяру и Гангуро", "48"),
+ SearchFilter("Двойное проникновение", "49"),
+ SearchFilter("Девочки-волшебницы", "50"),
+ SearchFilter("Девушка-туалет", "51"),
+ SearchFilter("Демон", "52"),
+ SearchFilter("Дилдо", "53"),
+ SearchFilter("Домохозяйка", "54"),
+ SearchFilter("Дыра в стене", "55"),
+ SearchFilter("Жестокость", "56"),
+ SearchFilter("Золотой дождь", "57"),
+ SearchFilter("Зомби", "58"),
+ SearchFilter("Зоофилия", "351"),
+ SearchFilter("Зрелые женщины", "59"),
+ SearchFilter("Избиение", "223"),
+ SearchFilter("Измена", "60"),
+ SearchFilter("Изнасилование", "61"),
+ SearchFilter("Инопланетяне", "62"),
+ SearchFilter("Инцест", "63"),
+ SearchFilter("Исполнение желаний", "64"),
+ SearchFilter("Историческое", "65"),
+ SearchFilter("Камера", "66"),
+ SearchFilter("Кляп", "288"),
+ SearchFilter("Колготки", "67"),
+ SearchFilter("Косплей", "68"),
+ SearchFilter("Кримпай", "3"),
+ SearchFilter("Куннилингус", "69"),
+ SearchFilter("Купальники", "70"),
+ SearchFilter("ЛГБТ", "343"),
+ SearchFilter("Латекс и кожа", "71"),
+ SearchFilter("Магия", "72"),
+ SearchFilter("Маленькая грудь", "73"),
+ SearchFilter("Мастурбация", "74"),
+ SearchFilter("Медсестра", "221"),
+ SearchFilter("Мейдочка", "75"),
+ SearchFilter("Мерзкий дядька", "76"),
+ SearchFilter("Милф", "77"),
+ SearchFilter("Много девушек", "78"),
+ SearchFilter("Много спермы", "79"),
+ SearchFilter("Молоко", "80"),
+ SearchFilter("Монашка", "353"),
+ SearchFilter("Монстродевушки", "81"),
+ SearchFilter("Монстры", "82"),
+ SearchFilter("Мочеиспускание", "83"),
+ SearchFilter("На природе", "84"),
+ SearchFilter("Наблюдение", "85"),
+ SearchFilter("Насекомые", "285"),
+ SearchFilter("Небритая киска", "86"),
+ SearchFilter("Небритые подмышки", "87"),
+ SearchFilter("Нетораре", "88"),
+ SearchFilter("Нэтори", "11"),
+ SearchFilter("Обмен телами", "89"),
+ SearchFilter("Обычный секс", "90"),
+ SearchFilter("Огромная грудь", "91"),
+ SearchFilter("Огромный член", "92"),
+ SearchFilter("Омораси", "93"),
+ SearchFilter("Оральный секс", "94"),
+ SearchFilter("Орки", "95"),
+ SearchFilter("Остановка времени", "296"),
+ SearchFilter("Пайзури", "96"),
+ SearchFilter("Парень пассив", "97"),
+ SearchFilter("Переодевание", "98"),
+ SearchFilter("Пирсинг", "308"),
+ SearchFilter("Пляж", "99"),
+ SearchFilter("Повседневность", "100"),
+ SearchFilter("Подвязки", "282"),
+ SearchFilter("Подглядывание", "101"),
+ SearchFilter("Подчинение", "102"),
+ SearchFilter("Похищение", "103"),
+ SearchFilter("Превозмогание", "104"),
+ SearchFilter("Принуждение", "105"),
+ SearchFilter("Прозрачная одежда", "106"),
+ SearchFilter("Проституция", "107"),
+ SearchFilter("Психические отклонения", "108"),
+ SearchFilter("Публично", "109"),
+ SearchFilter("Пытки", "224"),
+ SearchFilter("Пьяные", "110"),
+ SearchFilter("Рабы", "356"),
+ SearchFilter("Рабыни", "111"),
+ SearchFilter("С Сюжетом", "337"),
+ SearchFilter("Сuminside", "4"),
+ SearchFilter("Секс-игрушки", "112"),
+ SearchFilter("Сексуально возбуждённая", "113"),
+ SearchFilter("Сибари", "114"),
+ SearchFilter("Спортивная форма", "117"),
+ SearchFilter("Спортивное тело", "335"),
+ SearchFilter("Спящие", "118"),
+ SearchFilter("Страпон", "119"),
+ SearchFilter("Суккуб", "120"),
+ SearchFilter("Темнокожие", "121"),
+ SearchFilter("Тентакли", "122"),
+ SearchFilter("Толстушки", "123"),
+ SearchFilter("Трагедия", "124"),
+ SearchFilter("Трап", "125"),
+ SearchFilter("Ужасы", "126"),
+ SearchFilter("Униформа", "127"),
+ SearchFilter("Учитель и ученик", "352"),
+ SearchFilter("Ушастые", "128"),
+ SearchFilter("Фантазии", "129"),
+ SearchFilter("Фемдом", "130"),
+ SearchFilter("Фестиваль", "131"),
+ SearchFilter("Фетиш", "132"),
+ SearchFilter("Фистинг", "133"),
+ SearchFilter("Фурри", "134"),
+ SearchFilter("Футанари", "136"),
+ SearchFilter("Футанари имеет парня", "137"),
+ SearchFilter("Цельный купальник", "138"),
+ SearchFilter("Цундэрэ", "139"),
+ SearchFilter("Чикан", "140"),
+ SearchFilter("Чулки", "141"),
+ SearchFilter("Шлюха", "142"),
+ SearchFilter("Эксгибиционизм", "143"),
+ SearchFilter("Эльф", "144"),
+ SearchFilter("Юные", "145"),
+ SearchFilter("Яндэрэ", "146")
+ )
+
+ companion object {
+ const val PREFIX_SLUG_SEARCH = "slug:"
+ private const val SERVER_PREF = "MangaLibImageServer"
+ private const val SERVER_PREF_Title = "Сервер изображений"
+
+ private const val SORTING_PREF = "MangaLibSorting"
+ private const val SORTING_PREF_Title = "Способ выбора переводчиков"
+
+ private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
+ private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
+
+ private const val COVER_URL = "https://staticlib.me"
+ }
+
+ private var server: String? = preferences.getString(SERVER_PREF, null)
+ private var titleLanguage: String? = preferences.getString(LANGUAGE_PREF, null)
+ override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ val serverPref = ListPreference(screen.context).apply {
+ key = SERVER_PREF
+ title = SERVER_PREF_Title
+ entries = arrayOf("Основной", "Второй (тестовый)", "Третий (эконом трафика)", "Авто")
+ entryValues = arrayOf("secondary", "fourth", "compress", "auto")
+ summary = "%s"
+ setDefaultValue("auto")
+ setOnPreferenceChangeListener { _, newValue ->
+ server = newValue.toString()
+ true
+ }
+ }
+
+ val sortingPref = ListPreference(screen.context).apply {
+ key = SORTING_PREF
+ title = SORTING_PREF_Title
+ entries = arrayOf(
+ "Полный список (без повторных переводов)", "Все переводы (друг за другом)",
+ "Наибольшее число глав", "Активный перевод"
+ )
+ entryValues = arrayOf("ms_mixing", "ms_combining", "ms_largest", "ms_active")
+ summary = "%s"
+ setDefaultValue("ms_mixing")
+ setOnPreferenceChangeListener { _, newValue ->
+ val selected = newValue as String
+ preferences.edit().putString(SORTING_PREF, selected).commit()
+ }
+ }
+ val titleLanguagePref = ListPreference(screen.context).apply {
+ key = LANGUAGE_PREF
+ title = LANGUAGE_PREF_Title
+ entries = arrayOf("Английский (транскрипция)", "Русский")
+ entryValues = arrayOf("eng", "rus")
+ summary = "%s"
+ setDefaultValue("eng")
+ setOnPreferenceChangeListener { _, newValue ->
+ titleLanguage = newValue.toString()
+ val warning = "Если язык обложки не изменился очистите базу данных в приложении (Настройки -> Дополнительно -> Очистить базу данных)"
+ Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
+ true
+ }
+ }
+ screen.addPreference(serverPref)
+ screen.addPreference(sortingPref)
+ screen.addPreference(titleLanguagePref)
+ }
+}
diff --git a/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentaiActivity.kt b/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentaiActivity.kt
new file mode 100644
index 000000000..4b29428c0
--- /dev/null
+++ b/src/ru/libhentai/src/eu/kanade/tachiyomi/extension/ru/libhentai/LibHentaiActivity.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.extension.ru.libhentai
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+/**
+ * Springboard that accepts https://hentailib.me/xxx intents and redirects them to
+ * the main tachiyomi process. The idea is to not install the intent filter unless
+ * you have this extension installed, but still let the main tachiyomi app control
+ * things.
+ */
+class LibHentaiActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 0) {
+ val titleid = pathSegments[0]
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${LibHentai.PREFIX_SLUG_SEARCH}$titleid")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e("LibHentaiActivity", e.toString())
+ }
+ } else {
+ Log.e("LibHentaiActivity", "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}