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 zcmb7oi91y7|NniC8D@xV5tS|b(n#4-W=P4tCnQsmp2(zs3)>wVwv_4R(e&vnvVoo&TM6-5C6;&z8@ z+yzg@p9?1}xVw*riU1&!VrOIJ8D03j=pALNH?@skR-=CA?lZT^=G~1AwGCBclf7ME z24pqPUAH0Jv_mi}LdnsHmHO~W3yExn%(LVj=E4oahmzGv=iluVX%iDRZ<7up3*$c( zq<`w{oVb|w=I~&zKc(UuYa?3RuR*n>ulCP+S3$b}AL}9VTgcw)?iq*W#Jw6@(>#X> zb4T2$haQZ`){-@h)MWQl{Lv7Qk>8HD|K#y)fx5LYZ7|&PD)S0igeAen11;zSF!O5v zDs8?$Ezo}bv4O-H#&ETIO{b(T{>sF2MwMN8-|vrcl=b(#EzA6=@h!yFZ{vPMY90TJ zvZ3DcIJYKh9L)-ue-XrQ4w)Z~`)C{&HC7w9Y|u708q;SUr!<_|Pz6Y63@7CQgEWx>BjKey&$YuopmZFEFUmL+wy@&v~P zHZ!)c_*nfskzZ@Ewp{X=5?cG=Xhzqa+R)ISd$uUJ)MWjT-J*2sclXTIkko_a{@1q_ zobO_D?t}=Z7WS1ChC8v?^(SLR-j4PWzuhS2NexCUHv_`f?{XN3(<37FGcaeli49k2B(^8*~OO@3%q}lEnAn$`*!oi?q58D58+46%M}aG`=ve%m@lw60q#P z+=jhOT29m7EB3ArG?*1&BtcBl4YdO?Q<+X6MCStza+bIJ&zJ=V=C|VM%23_&3U{z+ zF)<5NUGhNQE7ZVfQo!-$C>fQ9906)D)!$ieJCIlNE^zxTeya{CO+54!#t={42<@H7 ztOlho5>ZU}P>YTY$SreG7^9zn)I<|6in43|F|P*qS{77E#!>ulGK@AQ9tpmmO8;*@ zAPYbframA`pkMRVf-1O}C{4NwRFmkT&|bC{q^8YiEGS&zcpI+DijiZNO??n|&ZT2u zcAq~BMiE|ssmZWyLD151&1nl|{nhqY*}>x_J$cz z#2lYfW!pVMPe=E9XYU_ZaCDA7MGhY|P7sa|Sgi%lTE@|$?5Iz}A5SWuIv?8J_Fj|f z9L>4Vk#cRydBY4(9kGF4X6T-irETCmE?`vwDtlRvL`Jta|I!iTx#HMZdKuWnSnx)w zTU^HIH!<4s+|qs}0%Y^bWvm1vsYfEAO&HvE?uYfFiOdX;?qJRSUv4|+|EK%WqFJXL z<8$XjcNqOA;KL}zeR<GxZ{o#op}aPk(cG?~|92eZ~#_PI>+vwc|-U1){j%fM`s(Mb>aWuzapJgbn}p zGi1eJseh1u%B6(5W+WowqSrCimg#fGdG2tqR9R1iZfkQ4w|;W=$#gn_erovcUc74> zpi(p1JrJX%*&}fMzvf#^qlWv+Sx*+|e9Q1+Vq*6fxx-htB)AXe4=qmj{Bv}wrNtx9 zZ)|rMH?0|L3O%xBc>SbZrg=afxEH~nZTDvZa&<9=57sW(gXd!f zPq5lN&B5J}O>310$Q>if!f4FS1NXq)IwNfvk|g&Yj@Bq3%^)1RF5fl{+&rjU1mFKt zu@sP^bZhvI7)^mHO}mLba->%eA)&46h=icW;zWZNPj6yKqIQTR-q3LOJ0$~*EF;p) z%M*ix>1Uvb7G+=P%HWKi9PRbj&|~l~@r+vTzh{5I+NmWNTmcC4lM$rhbF{T7^vIyI z6kBguvg_2Z7#)&snGG2J(*wjR)m1r0+r(36c!)LBtBTm8GW+&tNioEE-=r8l5B`&Y zb7BL$p8?$oZyjWQUO55k0Q4N-On6iwOH$`j8xuV-0^Twh*y0B10@e-&(v~Lv5W~dM zMIpx--g{{k@M~#tg;mm{sEV9H%QXL_UW!IEZ&B(F{+mI$0;m)6`<5%D89DRH_K>}S zi?B3pX7C6XS4}(@X;P-j6HZU>%%ujYLCJ`#`$9P~`D&hoOP z*e&s?>ydGd5p{JP9cvc~@?o>4d)mIp%9~9CC0myrS?n8CsQHPz<%u7Rzcw75!Dv5U zvptBcf88yZ0V{?DN8iN6>3&{GiBQ8v|Cq$Cp;J9WJ(j4$;LBy6?W>>jJ-k=Di2OI@ zb8ph+tt)@LyPm>;+h+uZ84Foc)*0&Z`XpoZ`#_DIc?YP`2qN*)Dlg%jWMU3{Ik{Py zV+Vf2?SG;psdKDaotZy$1WY-9SO7GV}!r7gIHx(#HOhp_OWoSX4 zL}`k=Z*nx4V~2EL+jmelizNQWOM=+A!;8=DfV7G_j7uKpy_LhO90vXzTR(tjUk8&| zCO+{gMiTc@22}@mavqSTMG5_(D&Y(gWf_tz_q{wrawZ7RnS3L*l%7f4 z^i%PqM$E7AOB#!VM83_8URESz-=dVhT-)?%^IG82YA5%+=J%qnp6^DLHmgE5DTMui zs()pT@FuQ_acrxUmhGKN0zOV>JQEcCpe~Wm=dGVg7)u-&*t*z_%Hma|V+-K_?$vJE z=I>q@9n~B4l3H^vHqz}1aGr7wep#}w(7uM>HN75`FlJ0tg2}0tSg?b3(b90I0Wnj5E(Dd zy?7DM5o^YwUO3m6C2ekqD7z{a3M5ei%DpuZM@|#9@{oXsOrGY-(yRrz0Sb(KI3O21 zX>2ZGeaqHB`ZBt`F*uNT2YU_QD31zA4f+%J*}p8+1hYoqnJ&ADmTy4PV5i?)wnKE~ z83gnR=EegOQ1E9Jzxx`q55^$^M1dwdxIH!C7LeJcjyOWeBuX;)oE)IT-GiCA{5u;M zVI;9l3FTjYjyQdW@Bba}44kbwCQZr#YDrNerH_0;F1tdG0cTm8E*f3Oe{7R_Jo@c{fTsg8lJ6!=rAj!Nu~mqwXte=+7ZNloRO`A;QdQ5m+OHQ zEnkQ6Yp&Fd(<#fq+enkLK`G7D4ivE4q`7lS3UGbWAs8o366U!1{SO}ueiY}^xvn58%Q0ktV9$c4&1;eiMahPyVhvw|JZ3Dto9n@lSU4OhNus|UAlpJ*K zuc9u4N7d0$*_it_z=P&p$^KDEojFWe@srLJQKBBw^={iv&at%43Om zwiM@%8E{RCxU?j*g?E?*0QNY}y5L-t8z?*?SUd0WJxpoTEx1yOi$qCQL;Dwcm0(U$7=<#2daxge;BrLjBnCAf9^H3+< zz%a!t?ARhntktsXKviI*+6@-a=3THpGSQDuV4f@gpj?D8^gvGjlgRb7LIfYb7L!f% zKJoLO0;bHk_pGdK2kCJd{Pg1MHNKxIQ}^~p(PnwBI^($S$ctiG%zZs+_Kh&8Ti(($ zMF0IV<;6sXD0(<-;iV_Sf2sI4g`avFcU|sz5QT)0Uw(j5Ljtkw{C%$A@|^rV6izLSep={^9zHz&tPTq ztX+!s5c_-ca2gy;bIf0%-?{$&34L;i1o+K%paqN0=>$Me#9UuWM_@@}NM(Bd7GCwU zG7C?p4Ep76gF(xVLF09EB#g5{mx$*L*1>^ZKQ>3mF%AH6a00A;t2E|$F)fk==(KiW zr#pFs6!!O<@(mCk?D4PJ*2Je>{G~mq-gK!-p+wg-q0RN-4ZO@-ep8POzO0iU!I3bF z`}Ap4ZR(ey1wuerG>$Aug5#gu7gA1io|-|+oDla4EBTfZ>%+-|@G5IA(y~Vb~gfq-nlW3$5 z{Akm40JF!3R^8{MQIRmAd@^7ha&mH7>gZ3+oZa%#l5#qXUst2?*7MHohW(*8wM*Su zL%g*Rr|+y|A*a{59sT#{=KR%J3cMM+z#U_=xF!f8!MB#)zy3@NY;6)dqGvtbLaf%X z+DcNce_sYPfg^N7*ieWpN4nx}Two9=T$rR2u6YpJkW`xug4>ema)kzKz2%W;Qmew; zC9`;yI`w*trG#+`;kX9)l}!Ag-a?4n%s8Z4meU^mI`4S6a5me+nqanl8+%vcK8XpU zPun$8t~HYV&%lwFb{!KLDbS;FC` zb|mi#T;Pr<#}_?Boyq%T3bCfB8hXGVOJ`_u($DJL$#wUt;mvok^>-sKVshv+_FUns z)xo1bouq_>Bf}#L7Kg9?{n{!sT`#wdW)XbD2q$ z+~2AO_(zRN8n6E1i0u`l)=cYo6>GMoNX$t$;tl*(VaMO 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) + } +}