New [RU] Hentailib (#10135)
* New [RU] Hentailib * -mangalib * option change language in latest * icon * icon microperfect
This commit is contained in:
		
							parent
							
								
									cb22cf119e
								
							
						
					
					
						commit
						9e00ce42fe
					
				
							
								
								
									
										25
									
								
								src/ru/libhentai/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/ru/libhentai/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
<?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=".ru.libhentai.LibHentaiActivity"
 | 
			
		||||
            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" />
 | 
			
		||||
 | 
			
		||||
                <!-- LibHentaiActivity sites can be added here. -->
 | 
			
		||||
                <data
 | 
			
		||||
                    android:host="hentailib.me"
 | 
			
		||||
                    android:pathPattern="/..*/v..*"
 | 
			
		||||
                    android:scheme="https" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
    </application>
 | 
			
		||||
</manifest>
 | 
			
		||||
							
								
								
									
										17
									
								
								src/ru/libhentai/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/ru/libhentai/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ru/libhentai/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ru/libhentai/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 11 KiB  | 
@ -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<Application>().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<MangasPage> {
 | 
			
		||||
        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<MangasPage> {
 | 
			
		||||
        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<JsonObject>(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<SChapter> {
 | 
			
		||||
        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<JsonObject>(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<SChapter>? = 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<JsonElement>): List<SChapter>? {
 | 
			
		||||
        var chapters: List<SChapter>? = null
 | 
			
		||||
        when (sortingList) {
 | 
			
		||||
            "ms_combining" -> {
 | 
			
		||||
                val tempChaptersList = mutableListOf<SChapter>()
 | 
			
		||||
                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<Int>()
 | 
			
		||||
                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<JsonElement>? = 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<JsonElement>, 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<Page> {
 | 
			
		||||
        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<JsonObject>(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<JsonArray>(pagesArr)
 | 
			
		||||
        val pages = mutableListOf<Page>()
 | 
			
		||||
 | 
			
		||||
        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<String> {
 | 
			
		||||
        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<MangasPage> {
 | 
			
		||||
        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<SManga>()
 | 
			
		||||
 | 
			
		||||
        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<JsonArray>(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<CheckFilter>) : Filter.Group<CheckFilter>("Тип", categories)
 | 
			
		||||
    private class FormatList(formas: List<SearchFilter>) : Filter.Group<SearchFilter>("Формат выпуска", formas)
 | 
			
		||||
    private class StatusList(statuses: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус перевода", statuses)
 | 
			
		||||
    private class StatusTitleList(titles: List<CheckFilter>) : Filter.Group<CheckFilter>("Статус тайтла", titles)
 | 
			
		||||
    private class GenreList(genres: List<SearchFilter>) : Filter.Group<SearchFilter>("Жанры", genres)
 | 
			
		||||
    private class TagList(tags: List<SearchFilter>) : Filter.Group<SearchFilter>("Теги", tags)
 | 
			
		||||
    private class AgeList(ages: List<CheckFilter>) : Filter.Group<CheckFilter>("Возрастное ограничение", 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user