Ism^-qFO2&H*P%d;)^eSYj1Dw$>DI=9|#0cB$I$>w^UUOY0~L5=I7@# z1LoPrx7>2e-@p9jFW(6uZkLpuOrNs}1g~wNyvfCfPM<~P52*OgaZx$WgIfBEsJ zoN~&=O-)V5nI!5^C^<($B!TiKc}8Qg*pm-G{O~PTU3JwHrr|tZB6TwMTpm(9Na2WJ z7gbrnE;Ur_*E*GAJ@#G_-lK#JhFx-YNfU9XFa)yd9j-uPp=40(`;1L2%`OTRAtaLI zDKK+GM9DBE=WO?QPoRQ}TBAW}41pk=KvDwbi7ZUXQ(@+lLxu6FGle0Jea$WsBESZJ z^9*SWhCD(DWc9%iMlHE6UVTXdfgy~+T!%;k!G$Q|RhLA;;Wa;T4&TIzCyoR^StKMR oBqSsxBqSsxBqSsxBqSvM4+3pC`;Wrp{r~^~07*qoM6N<$f+H<@lK=n! literal 0 HcmV?d00001 diff --git a/src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/comikey/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..68413a8caef3bba3ab2566afde1266bc7f28aa42 GIT binary patch literal 7413 zcmaJ`WmME((Ecq;x1b`qAgLfF&7ba8P$? 2G2j#W{S?6GsnCzC!% zWvu0&QN4VL{rvoV*}ioz7U!mUVy*PdIzDn7ce5zvc#l5b<04q|sMjO*P$|3y6Wra~ zwS0j6`0*p`3^nUX1wcO0vMR4Hf=(%VYk$O1F=H5ot>OFnm1sf}rmqZ!sDX Lw;^ &Hak&>bi&@**;!jzdHFIKs6KCQ)XB-IT|rJRj0vEz zry|V^YymJyNlEXhNEK-G9I5nNmxkIh?KwnM?Z}`O%m#>jqEu3%F1ghR{B=*M`Uf)r zl))}AMx%i9`u_Jv=`5i@WHu8!x&>%HjMz%k(4QyED~v{L6l+Oe91+JTIIN-)IFobe z5agOm0>C~7@ER|WMne Y-JXh3Zd` zA2kDXtRM%cP!;*^1d4=q!fm|HYlq0H(S4u*M3K7Xd32vTuTL@TRi_}kmLUXiSK0(< zJ_--@5fEN0{I;dKz!8gQpT$~@v&G(6lqA#;rA7-t`K>_`iStzJ14GjUrxcBVh59B) zbQ~-MQ@}*9+~lRMhI|bR)X$11j-W_=@CCrDR%*rUoCp3_RbQ{Jr$;q9Iy(LHCjo4V z1YaO|o1xUgLcIjM>`9xzR?i!t?${30et{!IV2^uy?8olw=6W;N_gp@H9dYfKBjL_e z6mb)z3zILz^ XYNf=@t~VI3A@^#4a6O5BzCEgMO+|_>t9C> zv4zA`(s 3)VND-COYLR2yTvIY;_TSXn=%g(p(qP`jjH3d-n>3v z?SIecNpzvgTPBfDHB;Fe8x^JeT~d_=nyKy`E6`oUPNy$rLubdxE}+SnJUmWwy_h31 zbZ31vc2+E(CO5jl6CN+=+shU%0%0yIJB6omaNc>+*yJM|j?=0QuJtNBE3lV^{$QI@ zLZcZ~3IbN_9|#u{O5}h4h4b%vpPsH1xLbby?AbG}KV0|}8EBd*N0T^S{{ux?Ma6o3 z7aJm$!LoekN_MXm4rfUHjl6fNsf7TMu)Ie#^n%Zq&=2 eBHsj4|&q+YKv((A(CrvG_^} F!AlOjS~K4QWX3W4ZwTFcuV+_6Rvk=CMEIQwy7R-l`=K$Cd0{Vcth zq~rz?*4sZovzp{uFFgj^39Vl}>>Y&&gpiG8O{j!~)Ntr*odth!o`Jcr8YqN8>bo&+ zuS1WyS_)*&B*qJ{)DmtcYDFz8d#Mk=CXn;xkS7uzufg2okifkhEvfTwHOE%dKYqkH z!h(SEa@XmFKP;~jUXuwf;o%g27XykPfW4p~s>uk*L(Ze%?zs!*JK^QBO34+Fh|2=E z+!NTs)h@564K|6$$=RJisa?j+pE($#>xkUB?F+|efEy%NWeK_(9Ob^KY392X S_9#oscThz|Vj}z49+WR^b zo+G+FQblNu*=sl& RO~6-%)1K-D9Kou*fW86GnKdfcS^+A$^6(%`!vP&Pg~ zo~V7n&)>S)eQpWdwW6J(MUmTC4~uLj937r?C}AF)PMdrON!cYLJh*c|7jh38q^(Md zJR%BK3hX)Fp?7k1wOR1X<|o_aEwKMsYls>IQND4k=UwxL rZlmRCu zK3W6rkR{huh+E<(Bik_t#LPVjvrf_#gurmxm;S~qTHI|#|iiU{rkQ*-&D+J+0R=-H8S$3|7e}};8(X| zX|EuLNYqtBT4(FN`%*hgDiG`BCXGuIR#UsaLyq1FTQ>->Tn>ZLU8ID|_igvvXDg6a z&Ja<3Ltab6m2bV59W{N}<}_1rmP%msG3}XQBaPDw<3sp(u2~~ AifrZSlVM zh?67$t?*-I-$TQ~g6K+2(^QVKG~_&ih@z)>l9J-y(^Rj8E#}nxMP5s>u8xpgGX*+< zXGTrn^VJ9$w7}Z0Uv9d&ClY)9ycK$~3@=|U=0s(~jNjKZ1QCynR0{)N+2Ehw0ylZE zI_x(~gBV;y9hYfw_sjLw#`3$re=jy)HHWAezA+Rnm{c`vm_v!lKY`AP2aTjRh}a@H zZNz)y`l5%dPqr3+T`621-QkmoNh*_l?K43!Jcb%lYNDWkKc%+LwcvTxa1(2J#T>j` zKcU}~nDtO^8Im{5-tjGzKK|)+Z8w`@s}~D2luKuV{2D!#=dt*x+ORH$+D>{e@~lf` z-HXe~sE9KCXkSu@Uh*A^(FtmDn=S`u@+SYxP45*TaqaS(hZ#D)Y-uT+T~kw2xnYGn z)jkYKHv8v5(ZVA4In1Kq4uYd?NRb7K%ClWQ-1QF-74KCa91Kwz2tvvcnr02G=v+Kf zDs7x^a!ftnj)>~PR{AAL0ljC0h(%*djKLAv@p^)=s=vRdlcv7D$}5NGCKCoQ39FYK ziK@vTu&209f>#H?jyc`t6xs^%C1}j7La4#n*R+-MLCB#Gr4ib-Ic(_gGk;lR^bP|G zBsD=jcve4i<|msF=a9ba0Pd!(FgRSc$xv|(&l{F1ZT4j&nGpN~K4N?N{fz8P^$Hba zcP>Xw^g*eMRo&m0y{6Ciw?e?=mDIl~qY-wqf=HeXKpjDfIGpvy${kFSN-LUGTQ_va zMG+@bP_q5U)>ymDizTzcb}X}ATs3TmhWVq ;+D=sSEJ|H(UqaKc%wJK>#)eGU z=KC=EYv4#_>fMIq;Cr>^=QXQ&EM!{g(wBQXheKQ&00O&z+N~kSCdml?(xk}kr=+79 zzI9$wvRsWEFwbmkTpdY%t1hm9PiQ#vi6k^L`zPYQ5Eye~&aYITOf_q9&hhi7w@N3Y z0`BPH&UjD?xA*e>?Q|VW |1 zY!!ZPlLrafn6+B9Hn+REV`BrlIW#TFm#F)m@A-OciOK^GR}l_68OUS4OJ@XOt10A% zZ;_C-V|S*~($hy!A>
%3paMXu#ALUD&SO=Zq@OuN*9%RAQT|=WL-(${f{H1F( zXdOu_N&6O{?o#_6=-6?_98OO6NAufUlO7fu?X_#~Y5k*wG5nI$(tzRNr US^Mocn-L=k}K)=vB*nO3hS_mbF{&f&F)W{TpT@p$l3y`ClYEe07Srw@Erqt zm*3 5bHw$DKM34ssg}^s=UocFAKUvUj@JJVGc}cw% zrnUj?iTFeMKYrct%%t&SAG9**cys^p;|LY^35OOipFHF*l2m3oW@qPd1k$9G+&Lvv z;B-HPc63w{`o*5;>i&~p28+TvC%4B&2I;dhk(Vr;L!>SmW1|P}PERd|hyTV#Q_B$f zheWNMi{a%lem0=mxa4Gq%&hSE`Q9Ey!nEk1*{OV!Jez@revdAy__5zJ{kD8J_n~;5 zChXvgP1nd8( 5!ID)1_+Y(mqFVE ziE76P@s_vpHrNRmp3ZFXvy!0Elu@2ECJ#1Y4EmrQ!E5sEf{MlOmwhk_=>?%E0ehLs z;-*&7U%#H_vribfiWTL-c4|~jLcWkjlfaY^VB$sRo3Ik_m#|>LtkTlbJH5D*c8TPN zQzBtujl*2Lj*`Cqt;L^g_|42w0dZ4PEXd!#Z=x?R_lKue2XtX8!jK=JIeED@*6kat zBgca5zfU|>N+I%Mhmqj8^#o2qrKw+`E=Nm|qoW^8nl5cKv$IQD>Rwe&17H&B!B6o; zDP?7pe%eOqc7_a(ew$WRRFDhg;0r!4hb8rI{PP?N{tPw=TZNzw$(2UiEv&xSUTO2m zdmbe|;i$G-6MoO;Lvkm))6#Fv^hL7*g7pf}&&QF+QFY7jt-yTcLiYVwM4w)z!5P;} z_1zcTsM8-*W-&5$zukLF^Qp}4K?iK){I&zWkAKbnR{8zC@rQ}udTtg7ef?Y5=?ap$ zbaO5Uy?xGq%~t!@ejm@B{Y#gpBXrHC? #L%yoa_!vFz*H zUbf=TE}Wg6W;sX$+B#TacmBbptIJJ^xQ`S?DVYNLy0KB)*Ys{fp8on~CIuG)$Vvj& znxgO6nNE%DU){-8^+fU%K88yM>MCh>$~AhxWJMkoy6_LeHf$TGrZ#vH01JXTuJOO+ z1BqWR@b##aP&mkugDG|J-yVj(&eCn?5 (iZIu z8eS8VmZtwvTvHRm5vqF1FR$IU 5%89jg^r*Ncm;9AkUKzTSrFELhw_YWPJ0ZUd-u7f*Hi zkg_&O_EEIyQlnf+u83N>)Bi0o*YY9iZ2<5rC-iBPcFfe2EOVyd=GUT~iv83wYdz9D znp`YLU$s-HquxdCzJaD@;$z35%67lwiIEkH3!IEi+g!h|QIiTYb6cvF*b^3s>plk` z^^dME%&(X^+ejX#r#Ip rk%Yp+TS+3JoavvRoU5g6ObW20i#Oj^`#%kD|xnsF{1-N>{04tg`%8*LX6MHMdx zN=)1ZPFu#1KHwyV^kiIE{&MxAq%rP4bMwdSB4WNNnXjc-NBWV-Z#+3?o}Ej({39~5 z`RpTm7}>)5DsN&r7eW(|FJa%w3Fh5D*^AIZ=w0kh*_OZ@Ya)wyZw^rjzB@y_JKQEc zXWv-N=H~|thN+*mk&x%|RNm}7psMDIB55OXydCD2|4>U}Qoi>KV8+;HOPo%^HMV|1 zg#68=_PYpynD|_*EqZm+eEMd2)p@zItx8JGsjLFSVbrFKM#YRfJ`uu;s2DraPYBty zR!r<9e3sjHfBBiny3e6W?f1dI&o4U<4i`Ny@Y6_!x4=w5;$Lq`dJ4GZQGH-3(`I0I zB>g($*`hmH=i=hhw|gD|{C06x+#{mFm_1h0Qi=(o4S>);-)L6xH_AQ5<^3k68AKOi z9FL0v`?UouwsxfYQa%p8$4XTBRP6%imLLV?7r9k#sj0DK6`I>m?7W4&o2$jxpT~gV z77|j@)qQXK9ht$-_28hbBHR@Hjv8(?(= axq-Jg|{`m3zPsR=F&C{wG#Gk0F zqh*uoG4 EVtbL)y+4^DrXfsb3-fHmPVQ~ V{e14% zuQzl1T3g0=A2zxIayQ47$jRhjF)`e2`2W!!LLOVZx!>x3#i*t>#Lj!p&c(gW!5! ah~l$VzcM1cBTU91n~B`P3h+0E5uvS?!VcotL5-JXe M$or#muqh;bd hW(fn-A+& y7Yzfp)TkdLEo4-!#pHp(>uO?|^|( z3=;H<{+2v$15F6A`}{gx`a?T2JUmCiss1fd4)cw6WCSY%GbYUqJP^K;R<`pq(;C@* zAai=}A&%pafz~mGQQ2ad*lqFh$zy(o0n|Qe-Yy8RI!EcG*E+m&u-n^bK)k^nU*#VK zF2D(P?Y898C6p?jM@| @5cTmvjZAtZd>H07NM4*t6qZC0x`IMKJ_u z+`j6w-_BaqSJT3bjc&%J0}|qu8p?VTkG&PpA*pydT!P~-M{~2*+bED9)H?)*6)vmK z^T=8^@p5o{iie55gskrBa{HOB>k8U{{09{ff9h7P&pwm*hIP^-gc8&fO$6Gx{o{E) zY>C9J1^0iOota$v`XP0KCqG14E@GXbN%4=BidzAVR(vYg4%+VDZ_4t%vHd$uiY(`J zA09<); H|>dr0Ty<5e9UpWNm4oixy&?28lc z1!!(jZ?;piynI)ops@rs6ONBsZcmMp4M&JsLKA0ZzK+?raV+-i&G-=km!9+N4M|Ir zo>ROgc=Z9CAlwZla)_*DUblA*zR4PZUy&wget|-X1fU |Aon*@H4GW;(>pPk)Y`#*D_ zyrN>qGp<6+3&Aj3z)`v{K1Ur1b$i4#I3|$#M3`cWAob#wDqWRv`BgQ>&TcIlez6|E zd&~Y8G39{%k&Y7t1&tFt@~!@H%RNNg4mJ{$2W}}{fLXO7!HsBZTw`oyvK1&6fPxjH z?o*h2vF kZ!2fuVfqwmP z+98~8-?N=_n1{G8G!H}bjF$sXF@DSEJL+3~3p_6Pz$}^0zzL#iU-t0mP-*^ha(=89 zlRc~&6eOMSp4OB@RK-q%K2TZd6rD4he7F- z&1&$@XMBf!3|w;_dMFYG6^j$E4;KrCJ!;2AVM@+ z2c9m3Gn9Y>!G0alGSIufK%_R=yQBo#F;eNgcb)qU{Gja%_mn@$C{{5vx#q!IfvkOi z0CA7Uok%&OWO*z?tL#Q3Qny@pI!b8&r(h~nIn97`boo ().getSharedPreferences("source_$id", 0x0000) + } + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers) + + override fun popularMangaSelector() = searchMangaSelector() + + override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element) + + override fun popularMangaNextPageSelector() = searchMangaNextPageSelector() + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/comics/?page=$page", headers) + + override fun latestUpdatesSelector() = searchMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector() + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable { + return if (query.startsWith(PREFIX_SLUG_SEARCH)) { + val slug = query.removePrefix(PREFIX_SLUG_SEARCH) + val url = "/comics/$slug/" + + fetchMangaDetails(SManga.create().apply { this.url = url }) + .map { MangasPage(listOf(it), false) } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/comics/".toHttpUrl().newBuilder().apply { + if (page > 1) { + addQueryParameter("page", page.toString()) + } + + if (query.length >= 2) { + addQueryParameter("q", query) + } + + filters.ifEmpty { getFilterList() } + .filterIsInstance () + .forEach { it.addToUri(this) } + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = "div.series-listing[data-view=list] > ul > li" + + override fun searchMangaFromElement(element: Element) = SManga.create().apply { + element.selectFirst("div.series-data span.title a")!!.let { + setUrlWithoutDomain(it.attr("href")) + title = it.text() + } + + description = element.select("div.excerpt p").text() + + "\n\n" + + element.select("div.desc p").text() + genre = element.select("ul.category-listing li a").joinToString { it.text() } + thumbnail_url = element.selectFirst("div.image picture img")?.absUrl("src") + } + + override fun searchMangaNextPageSelector() = "ul.pagination li.next-page:not(.disabled)" + + override fun mangaDetailsParse(document: Document): SManga { + val data = json.decodeFromString ( + document.selectFirst("script#comic")!!.data(), + ) + + return SManga.create().apply { + url = data.link + title = data.name + author = data.author.joinToString { it.name } + artist = data.artist.joinToString { it.name } + description = "\"${data.excerpt}\"\n\n${data.description}" + thumbnail_url = "$baseUrl${data.fullCover}" + status = when (data.updateStatus) { + // HACK: Comikey Brasil + 0 -> when { + data.updateText.startsWith("toda", true) -> SManga.ONGOING + listOf("em pausa", "hiato").any { data.updateText.startsWith(it, true) } -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + 1 -> SManga.COMPLETED + 3 -> SManga.ON_HIATUS + in (4..14) -> SManga.ONGOING // daily, weekly, bi-weekly, monthly, every day of the week + else -> SManga.UNKNOWN + } + genre = buildList(data.tags.size + 1) { + addAll(data.tags.map { it.name }) + + when (data.format) { + 0 -> add("Comic") + 1 -> add("Manga") + 2 -> add("Webtoon") + else -> {} + } + }.joinToString() + } + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val mangaSlug = response.request.url.pathSegments[1] + val mangaData = json.decodeFromString ( + document.selectFirst("script#comic")!!.data(), + ) + val defaultChapterPrefix = if (mangaData.format == 2) "episode" else "chapter" + + val chapterUrl = gundamUrl.toHttpUrl().newBuilder().apply { + val mangaId = response.request.url.pathSegments[2] + val gundamToken = document.selectFirst("script:containsData(GUNDAM.token)") + ?.data() + ?.substringAfter("= \"") + ?.substringBefore("\";") + + if (gundamToken != null) { + addPathSegment("comic") + } else { + addPathSegment("comic.public") + } + + addPathSegment(mangaId) + addPathSegment("episodes") + addQueryParameter("language", lang.lowercase()) + gundamToken?.let { addQueryParameter("token", gundamToken) } + }.build() + val data = json.decodeFromString ( + client.newCall(GET(chapterUrl, headers)) + .execute() + .body + .string(), + ) + val currentTime = System.currentTimeMillis() + + return data.episodes + .filter { it.readable || !hideLockedChapters } + .map { + SChapter.create().apply { + url = "/read/$mangaSlug/${makeEpisodeSlug(it, defaultChapterPrefix)}/" + name = buildString { + append(it.title) + + if (it.subtitle != null) { + append(": ") + append(it.subtitle) + } + } + chapter_number = it.number + date_upload = try { + dateFormat.parse(it.releasedAt)!!.time + } catch (e: Exception) { + 0L + } + } + } + .filter { it.date_upload <= currentTime } + .reversed() + } + + override fun chapterListSelector() = throw UnsupportedOperationException() + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() + + override fun fetchPageList(chapter: SChapter): Observable > { + return Observable.fromCallable { + pageListParse(chapter) + } + } + + override fun pageListParse(document: Document) = throw UnsupportedOperationException() + + @SuppressLint("SetJavaScriptEnabled") + private fun pageListParse(chapter: SChapter): List
{ + val interfaceName = randomString() + + val handler = Handler(Looper.getMainLooper()) + val latch = CountDownLatch(1) + val jsInterface = JsInterface(latch, json, intl) + var webView: WebView? = null + + handler.post { + val innerWv = WebView(Injekt.get ()) + + webView = innerWv + innerWv.settings.domStorageEnabled = true + innerWv.settings.javaScriptEnabled = true + innerWv.settings.blockNetworkImage = true + innerWv.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + innerWv.addJavascriptInterface(jsInterface, interfaceName) + + innerWv.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + view?.evaluateJavascript(webviewScript.replace("__interface__", interfaceName)) {} + } + } + + innerWv.loadUrl("$baseUrl${chapter.url}") + } + + latch.await(30, TimeUnit.SECONDS) + handler.post { webView?.destroy() } + + if (latch.count == 1L) { + throw Exception(intl["error_timed_out_decrypting_image_links"]) + } + + if (jsInterface.error.isNotEmpty()) { + throw Exception(jsInterface.error) + } + + val manifestUrl = jsInterface.manifestUrl.toHttpUrl() + + return jsInterface.images.mapIndexed { i, it -> + val href = it.alternate.firstOrNull { it.type == "image/webp" }?.href + ?: it.href + val url = manifestUrl.newBuilder().apply { + removePathSegment(manifestUrl.pathSegments.size - 1) + addPathSegments(href) + addQueryParameter("act", jsInterface.act) + }.build() + + Page(i, imageUrl = url.toString()) + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + override fun getFilterList() = getComikeyFilters(intl) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = PREF_HIDE_LOCKED_CHAPTERS + title = intl["pref_hide_locked_chapters"] + summary = intl["pref_hide_locked_chapters_summary"] + setDefaultValue(false) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(PREF_HIDE_LOCKED_CHAPTERS, newValue as Boolean).commit() + } + }.also(screen::addPreference) + } + + private val hideLockedChapters by lazy { + preferences.getBoolean(PREF_HIDE_LOCKED_CHAPTERS, false) + } + + private val webviewScript by lazy { + javaClass.getResource("/assets/webview-script.js")?.readText() + ?: throw Exception(intl["error_webview_script_not_found"]) + } + + private fun randomString() = buildString(15) { + val charPool = ('a'..'z') + ('A'..'Z') + + for (i in 0 until 15) { + append(charPool.random()) + } + } + + private fun makeEpisodeSlug(episode: ComikeyEpisode, defaultChapterPrefix: String): String { + val e4pid = episode.id.split("-", limit = 2).last() + val chapterPrefix = if (defaultChapterPrefix == "chapter" && lang != defaultLanguage) { + when (lang) { + "es" -> "capitulo-espanol" + "pt-br" -> "capitulo-portugues" + "fr" -> "chapitre-francais" + "id" -> "bab-bahasa" + else -> "chapter" + } + } else { + defaultChapterPrefix + } + + return "$e4pid/$chapterPrefix-${episode.number.toString().replace(".", "-")}" + } + + private class JsInterface( + private val latch: CountDownLatch, + private val json: Json, + private val intl: Intl, + ) { + var images: List = emptyList() + private set + + var manifestUrl: String = "" + private set + + var act: String = "" + private set + + var error: String = "" + private set + + @JavascriptInterface + @Suppress("UNUSED") + fun gettext(key: String): String { + return intl[key] + } + + @JavascriptInterface + @Suppress("UNUSED") + fun passError(msg: String) { + error = msg + latch.countDown() + } + + @JavascriptInterface + @Suppress("UNUSED") + fun passPayload(manifestUrl: String, act: String, rawData: String) { + this.manifestUrl = manifestUrl + this.act = act + images = json.decodeFromString (rawData).readingOrder + + latch.countDown() + } + } + + companion object { + internal const val PREFIX_SLUG_SEARCH = "slug:" + internal const val PREF_HIDE_LOCKED_CHAPTERS = "hide_locked_chapters" + } +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt new file mode 100644 index 000000000..ff3de86b2 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFactory.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import eu.kanade.tachiyomi.source.SourceFactory + +class ComikeyFactory : SourceFactory { + override fun createSources() = listOf( + Comikey("en"), + Comikey("es"), + Comikey("id"), + Comikey("pt-BR"), + Comikey("pt-BR", "Comikey Brasil", "https://br.comikey.com", defaultLanguage = "pt-BR"), + ) +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt new file mode 100644 index 000000000..108104533 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyFilters.kt @@ -0,0 +1,68 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import eu.kanade.tachiyomi.lib.i18n.Intl +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl + +fun getComikeyFilters(intl: Intl) = FilterList( + Filter.Header(intl["search_use_two_characters"]), + Filter.Separator(), + SortFilter(intl["sort_by"], getSortOptions(intl)), + TypeFilter(intl["filter_by"], getTypeOptions(intl)), +) + +fun getSortOptions(intl: Intl) = arrayOf( + intl["sort_last_updated"], + intl["sort_name"], + intl["sort_popularity"], + intl["sort_chapter_count"], +) + +fun getTypeOptions(intl: Intl) = arrayOf( + intl["all"], + intl["manga"], + intl["webtoon"], + intl["new"], + intl["complete"], + intl["exclusive"], + intl["simulpub"], +) + +interface UriFilter { + fun addToUri(builder: HttpUrl.Builder) +} + +class SortFilter(name: String, values: Array ) : + Filter.Sort(name, values, Selection(2, false)), + UriFilter { + override fun addToUri(builder: HttpUrl.Builder) { + val state = this.state ?: return + val value = buildString { + if (!state.ascending) { + append("-") + } + + when (state.index) { + 0 -> append("updated") + 1 -> append("name") + 2 -> append("views") + 3 -> append("chapters") + } + } + + builder.addQueryParameter("order", value) + } +} + +class TypeFilter(name: String, values: Array ) : + Filter.Select (name, values), + UriFilter { + override fun addToUri(builder: HttpUrl.Builder) { + if (state == 0) { + return + } + + builder.addQueryParameter("filter", values[state].lowercase()) + } +} diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt new file mode 100644 index 000000000..02c4073ad --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyModels.kt @@ -0,0 +1,85 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ComikeyComic( + val id: Int, + val link: String, + val name: String, + val author: List , + val artist: List , + val tags: List , + val description: String, + val excerpt: String, + val e4pid: String, + val format: Int, + val uslug: String, + @SerialName("full_cover") val fullCover: String, + @SerialName("update_status") val updateStatus: Int, + @SerialName("update_text") val updateText: String, +) + +@Serializable +data class ComikeyEpisodeListResponse( + val episodes: List = emptyList(), +) + +@Serializable +data class ComikeyEpisode( + val id: String, + val number: Float = 0F, + val title: String, + val subtitle: String? = null, + val releasedAt: String, + val availability: ComikeyEpisodeAvailability, + val finalPrice: Int = 0, + val owned: Boolean = false, +) { + val readable + get() = finalPrice == 0 || owned +} + +@Serializable +data class ComikeyEpisodeManifest( + val readingOrder: List , +) + +@Serializable +data class ComikeyPage( + val href: String, + val type: String, + val height: Int, + val width: Int, + val alternate: List , +) + +@Serializable +data class ComikeyAlternatePage( + val href: String, + val type: String, + val height: Int, + val width: Int, +) + +@Serializable +data class ComikeyEpisodeAvailability( + val purchaseEnabled: Boolean = false, +) + +@Serializable +data class ComikeyLmaoInitData( + val manifest: String, +) + +@Serializable +data class ComikeyNameWrapper( + val name: String, +) + +@Serializable +data class ComikeyAuthor( + val id: Int, + val name: String, +) diff --git a/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt new file mode 100644 index 000000000..68b2bfac3 --- /dev/null +++ b/src/all/comikey/src/eu/kanade/tachiyomi/extension/all/comikey/ComikeyUrlActivity.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.extension.all.comikey + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class ComikeyUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val pathSegments = intent?.data?.pathSegments + + if (pathSegments != null && pathSegments.size > 2) { + val intent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Comikey.PREFIX_SLUG_SEARCH}${pathSegments[1]}/${pathSegments[2]}") + putExtra("filter", packageName) + } + + try { + startActivity(intent) + } catch (e: ActivityNotFoundException) { + Log.e("ComikeyUrlActivity", "Could not start activity", e) + } + } else { + Log.e("ComikeyUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +}