From 73be84037babee5c820f53a3cbeb8561995a1300 Mon Sep 17 00:00:00 2001 From: Claudemirovsky <63046606+Claudemirovsky@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:09:49 -0300 Subject: [PATCH] =?UTF-8?q?New=20source:=20pt/Taiy=C5=8D=20=20(#799)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Create Taiyō base * feat: Implement popular manga page * feat: Implement latest updates page * feat: Implement search manga page * feat: (finally) Implement manga details page i hate it * feat: Implement chapter list * feat: Parse page list * feat: Rate-limit requests to image CDN * fix: Fix crash when using URL intent handler * chore: Add source icon * Optimize icons with pingo --------- Co-authored-by: beerpiss --- src/pt/taiyo/AndroidManifest.xml | 22 ++ src/pt/taiyo/build.gradle | 8 + src/pt/taiyo/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2476 bytes src/pt/taiyo/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1780 bytes src/pt/taiyo/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3108 bytes .../taiyo/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5400 bytes .../taiyo/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6826 bytes .../tachiyomi/extension/pt/taiyo/Taiyo.kt | 274 ++++++++++++++++++ .../extension/pt/taiyo/TaiyoUrlActivity.kt | 41 +++ .../extension/pt/taiyo/dto/TaiyoDto.kt | 79 +++++ 10 files changed, 424 insertions(+) create mode 100644 src/pt/taiyo/AndroidManifest.xml create mode 100644 src/pt/taiyo/build.gradle create mode 100644 src/pt/taiyo/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/pt/taiyo/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/pt/taiyo/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/pt/taiyo/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/pt/taiyo/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/Taiyo.kt create mode 100644 src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/TaiyoUrlActivity.kt create mode 100644 src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/dto/TaiyoDto.kt diff --git a/src/pt/taiyo/AndroidManifest.xml b/src/pt/taiyo/AndroidManifest.xml new file mode 100644 index 000000000..26b60d15a --- /dev/null +++ b/src/pt/taiyo/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/taiyo/build.gradle b/src/pt/taiyo/build.gradle new file mode 100644 index 000000000..2e28fafce --- /dev/null +++ b/src/pt/taiyo/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Taiyō' + extClass = '.Taiyo' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/taiyo/res/mipmap-hdpi/ic_launcher.png b/src/pt/taiyo/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..790174cade06e28ab1e30ed225c806700cc7b665 GIT binary patch literal 2476 zcmV;d2~+loP) z^vr1D$RvM0_X^S}RqN2{t-KO?mP*=Uy6g+ z=;&xqZ|}e-E0yxCL27d21aR67t*58w$EDKQyH}LLoWynRa-6PPPfb;>^}j+^vLKRa992*3is;sUZf z&n*BVB($?P5SpuC}XGbZy%-Zs>0G;`i^ z%u#wkLI9jgjdL^&iJnW^)!QWp#G{2(vNxFgVU1psM@?Yold*x`ErOD{N1AbvKAfWG z96b=5G2}Ui#Q+No5`ZrSJRvdb`|w&3W(l0cp_LIh^t~^Y!aW8D@WP4J`0$oZ_~Q2M z`1;j$;2Wx_ZB@4!FPIp|T?YEmtB!4#?AE}EeomG%+-GnI4;&uGST2jaWq~u+)UMa> z>cot4(ZOmJA3SmtpFMFBuCX5QbMXO5DOBMtbJB1+jh9VM;I@5zuow%C`v2VOKWfgK%ANTSnJmZQnq&aWAogNNJgzl373YLZEOpGJN7-EZEY@-k&$y`i= zv4~JO0I|6K0V=7EWU~kq1CG9TV*tPy)~QgtrE=%D&YX?s42d>`n7_E511hw7TL4F| z2ab#wo!9XMV6>Xd=P;hj#)T6b*2ZFirYec%g2{Wa_&I4Pou*N;WE(n~FtbSkV23 z=WzrZ{-QhT7f+tVjeEMWDw~A`kbEg|tX9Jh z)dSvjXaFDBv>C(Mi?D^16kgPWsZUo*3gNmCD7!B0yLnF!o$nd##XJC3sGG>vLz#?n zM2V86daDai2o47W0|BJ^qGi#5Zfmhfq0ygC-@_bYSI0Wq?BvmgK5;xuJgbMHD^|~QIc6f+g-Er`? zjq4Xi%;cCfBiA4>-1!F6Tdw&&m5R5JR7-gOkt5h#EZ`o4gXE_5g#tcw^r&*5wQxPk zHIGyItk1S7hkgCjY5cBISvZcCZo4}iWVwZ`#gXCkQV1*pP`(7}lH5gLpZxFt;OFJa zH9n`DmQly18B>nv008BLX}tT;VM?~QQi=8vR~f}? z_wA<#eBPP~`uy_AQ~0|&?hgI^c(~eszCHB-ym!e9XEx&b5-n~o5SI1O*zt~pM_?WGhHDd0HhhCTv#AD7Qs?I z;AKzJ^cH_wD#aQvSqwD$?n&9WrgGAQMn)*dtj^^ql=jcfkt5bAkiF?NIq=U4%Eyi# z$1l{q&bTg(Z`=se3ITu{=;lpLTuGjgC@x&=w5q$2%gbITk`W3A6`RkzC z|E2>6@c9!bF)wAzUvHDcNj{rTzHUuU_~z*|${l6gPj&nE8XBae`$YxiS-D^=wD+HPu@&NXl&rUg)_TfG_~ZT!?!Y5<98T_TghF+bHv z4o6=nTG}S_Q;p5qGXmY0*vCuwE^j2SreF3kawK|^b*_>3{jz^K}+ zV_2ki3dUqjmoqr@!=QZtrqzX_cJD2OBVk^wDg^fEZpJkASpZ{&g7bdMvTkT{T+}WJ z;s3r`oqHgF!vLxPWE>-=0CWPFRFK}mIo}XKixF-Mj)G}8ao2!^y03DM+&Mo#_jrID zGzFj@H&S^3!~rm{J<^Mpwy!MV*$OB>`Mx#^9*VXz#&5p3W^KB3yNsf qKR00wWjRVVlohRLMJxKh)_(zCyH8UNQ7 zwfrS@T_1*Fx~%KwR8=)qRY_%8K~dC9NqG*GWyPbM()28$B*`d_Q;4F3VkQh@2!aTF zKLpPUz;!+HT_3&UoVjG%aANl^R8>8!ewiIB`tIR_06fosgb$uR0rmkHmO~a}nl_n+ zO@?8CuA8K32B@kIilTums~|}Vh@uRFAdw(S#PcF>yujYJuU4yj%IP8vLKunx@YoV} z=c-(I>;%QBrMAe&_D}klUN!~1Wwrl?KeOQn8Q<*qhQ-$>zS{8W0vSnTO@Srh0uO>c zah$wYo6X%PVEq#h*KgG8FWK;0GPXd0UVz2NTh0Q$?~}!rn*N8!lI(1s<$lgZ0nzX*PTb?>~1dKyJDM6H@@;$L-sk_;$nV)_4Db zF`jt=O#yPezybz%boZ!$J3cb6zr;rc@Cj+)mUw-EnH6)2a-|8Q==0dNZQHhO;qkX^ z+qP}nwryL;xu+|sL3M}gKJ1{ore~k8xZwfaPp_cY6UdU`tIP4*P2`4Z?F0m zc@GaKmeCFA2xD=%OwMOyIV7)EpYj`Y#>QFX18h{^#Q;(`T#=VtT2<|WeBZVJN9W}` zQhnzFP}H#M>L&Fax(;WubEu7U1f#a%;<` z>j9*&iWHaB$0cB|oIH}dNw%3ivvW4sBP-iXrbd=v_l!(I8CN?Y?_)T0Tnk`JdL$(k zr7n=#KsxRyITT;dO99}vN|%kJO9+zr(0ou?Cr%ZH8t3WJ#c(s5w5GO!nGO| zmAJN?@Ll7Cf&%Qr>fg-IKX!Eie3qGBF+YCduuAj~6am~Q>~atACk!F19)s&*2_J8^8~2 z_aH-`&*vPQn~MV&Y!|jE4-J>Xt_P6Pk`(RAq2w!6g3u?_-aRDU*a#w zUOU<|3*(4^7>b&iJsf84B2-pd3HIj(l4y@~9@)#ow=?e;p5$*_D4=VIFcn9}hr|^- zu37R3*E(Mhk9da(LTP+o;hb1a8evD26&{$)i=#Y;T7N5qqB7zQ6GkTFi;euc03t^) z+7D3SW_;guW9e#2yj*s@CY?t)B?@r?Z>s{9vXoUkxE-%)WOQSrzXV3Po|Q&%l65Mt z9vSrl@Y*PfPDVslTr8i8q}7*z3-lm6ps<`YiWdTLZ3fm{=TBTf4IlmxON%?6Su7w) zvUlbtGvOBl=V_Y%lw~!b7-2F7Syo7ZpG!oaEs7yY(l6!&B(l@1xsy73aDN5d0S|iQ z;?G3e&!Z*y{{6~bka{VRNYcjpw$Vd3&Ckl&5n&>R1^ WI9CyXF~*w!0000@T1sicxhYRHKZ+|Mwy#pAC1ZU8OPSj-iJ!4PW$_!0^#2e7J7 z;GMWDr#C*p)zvZZ(I@zD37Z+}+ol#s|d>8;z zg|DVE`vI6D=I`;(nCQ3lNucBcNJRDdhJfy6Kea5$pR2blm23J-`@KrZ0B2IMFJ^wwL{ z%XW$oB%2HP3Uz#9X@fp_tN$4W&5Tfo9})5F9+;&qK^aN{p8^IwPy*l-GF-ec0elbO zdTdFs4F(67?g8)1ceEQOlKK#(jAOKGQUjc-7@Vqxb ziaK~w`0<*QmWnO%@-WQrM~-PC!_dJ2v|1rFT0t!B>c$&&_2Sy)b@HY)Ko?Zu(|bd? zfPKvHa%`TLC%p+`en<}X4}fMmy`*84&ePBinhzo$2H@}$}qD!2`b$% zaNLOD*tw_>$NcdpK5uUEuKJ;02hhW30L~dT62}c6rUYBHIw%VS@WQN_IQ_2$c(}5X z0-pDKLF%hxt_=UW@nc=USCh^dZkRY8C&i8L`O299JV_onO~mYdN=n?p=Un-|^T&?C zp2bB}p~REDAQkX^g1>2Q4o(?9oM1($*5}5F6EHa~&Etie_c=f++b~4v83!;m*c9?J zhC3%u#OB}qfM8^@c%B2i3PmT1m|X1l_j~Ax|9)zEx(r3+v)}t1pdN~{bRGMZ6stjG z1n^_T7{fWEN5RJ!1x$jKum_>&@bWTQt1Cg|s_@kfQsno`{PRYA!@u8Rb3p6AWUW!n zcJRK3qAm0D-G&{qb;5q7CF;i$)x#CF2hgvMQ#B2f)6@D5zN+SAWlf?U?E1h22fAHBLjhiplIN;KHa-j2VllxrUhlLvM7{;e-FD*T@qT zyZ`jFs?oeJL*4YxH%<684PB0n_K+Rj0={73BCMa2BY(eI?}2j{eKphuyYYTQW7JLo z#*k(h2xtsJ#}U7`2w>9~z%`&@fdJl}I~%#C8SPWk(}UmI+wntd8&-CA%NYDedk5NW zJ32kJ&hb@KGd^i*!bhV0E)23=K>_wHDaM@4 zOji}tkFBkEys`>UR#jteW`CCvJEqKrv~7?zpvI%2n#wpa9CLx-WMJ? zAuTO3yVXnG^@O1gEi1+S6_tZ(9Uzs$7 zG#opu9M4zR;Hss|&&@G6ygK6H-$p(BXj;lmvVz_W9+5_Y_@&k7)>T z2bP>=HX5uTD!O~HP(XOQz5(^(dlO7nMw{j3%GjT28gka=7zQpIHwIfa=i$KLe#i9_ z#>;+TeJPUosejGKwG+o$8yXShjy!QUMi_p0Ab_=m;qy%& zG7ST!#$YgpplzemcF-*Dt?ubTrFf6ax;n##levk=6!ecIw-+Jj-YJuyG1k{X<+zS$ z9gc(dgfU*It;OX_mWt{m4SN+Ahdo4&yMciI+>Ghi|JUC{f}aQ-OqR3H%$$L80W>m} ztgxP`t`_iH<)qk1B;eCV3`d%-%W;^CXAzFL9wv^w za!dmUiv)W0_;F-g8(Jhc4b&VUAgz*Hy9Q&j?rye1XbM?qkx9*#?yrH#->2#t@=P;a zqRMNT-;bG@8JL`&j@Yz0J}nJbjvp&$-wOilg<`gc6XU$>EL<>V3^WD_|HqaVyd-`f zuJ7-RcwszH2ONL3ci{V$R=NIYv@A4;Rv!$<|B&3FwK?F^!bO9DEJF{k)TJ@=eLk5R zZeO`-z#dPRG4qneOIGV)O{Sq^c18wfWMs;UXG%tTboL1kkSCqrtgFYvt156{d6|U# zSVe^_Q!f}h26|knxu~NPPl-_UQ(GIp72ul$aQFL7^kNDIN&%3SmshZ|A2;d}ZW=Q{ zKDggDKOaX7E0^99k(|AsH#W&xE%Jg)tg(srfmIdKV}2Ik9R<+T@H;x%h688;&j6CG+6f>P0GQ|DC^|<6WVqoR9Z_xs9>BwmF}8$r zKGiL7`W(Oja12Cg8kPWXC=9?O138vuy{7B>R1b2%pi8o$ko_tETjig+F@;d*1CR!w zC_O#%Wlht@Qh}rg@4Xzyncvmbu`_@=0PO&R037^_bj2M94RUb%YvBd!0W60xMi~DG zKX`et%;5{WyL*lToGJiaaqwN|bRB>%l7KS+@T%aOrRCuapm2+L2xW0000beBj9 z(tW*e_uORrkhhvWWiQdTCeGw$dyTJ}?txy!pVL)Y#&@q-5y7kk0fY z>bc5Ezs4tOlWObUXlj0=^8atC(Nd;EJmEj2Z8QW|nk>#dRxok#*9<-?Y~B_DI2C;m zi%K#)k{q&z-5%yN1v%?(M)OY+lo&fb;z2JarxzESxid%Px;&O{0dpD)UKO&*HNCSF zN;9kR$%oeh^0oCGUqt_2YkPwmvDZX+PZk5(&X&W&clnEoiaY^#zDUjm0BZPszs$<{ zXGNgk;7*L6APw=nUMX<(vN80z#gZx@{I-7Ehty?%*_Z%p>@-1>D^b0Wc-Q-8wmR*! zT3`Fc_9_v3;RM^?e?fki&8zoV;flMV|2||h?PRDFxys%#Wx4Lqg)HT5zPA)n5j{y z_cSRfX_JG4BfDwkyz(-^YfSd?j2!fmNM2hB0n+oEt%L?nJ6%tEd3kBAB?>A0#8++~ zCw-|~Y5yY6 zvC-gH^I zVm&X-N@^K;hq%5QtZ>!yTJ$%})5YMT~_MvDyHW2HKI%d(>~<{Yz% z&%qS*CmN`uCXDb6Y}J*XrTDV7KPnhldSFYk^rPByl2507Of_&=&^KFva7+1Z)Me@S_=w9Qn_u?arE~ zB%O!5yis4#kL+jUJeTyK4jk2w8y=iY_#S!@{m-*`dKD2?=2Tp09NV^OkFXA{8q+0o(8n4H{7J`iGlQo9bAgBiD~29Ne*PN+$)A9b^6M|@m&A`Gz!0Ch)6nMmX6#*in_M!@(KSZOg&SCTZd+^{>DNjx!Ui7J`D{vSflQ&5S!+URV2Fd4ygq zTBBWO_BMX`U28IAOc)^%(0xNN0+ijp;^VQZuigEuH!s-G>7H^Z*4+hvinqnr+FApW zTx6XWihsOxD%cT#b|Q{?y6$M+M?;H^Vqtn4Db`KdIwF&Z4iE^mGbWI&j=dI6h-}av z@55S5QQyc-_UQehihAed0gbEb59_$cyE*CJrPoXuZW0kpdY%yLpqz(4RSrg`9?s`m z$DBtV03agP?}KWym?4h;iS3021+H}5Re%5ebv{#b!_u3(%29qA)M64)~rV8G;}w4bM5^+35bm(RHeKY8YLbj6Q6OhPWW^? z#98|F;~uD-Y2N}S5FvB)TXg*IfKNTY?#pRkf%Gi}BWMC$V?)SH8qcQG-JBLsNSaXR z%-2mxSQqxQ+o@t^6kvL3_3e7Ce9#JF#|oJUFYdf($QsSdO$Q6Sw>R6LQ)PGkXD`MR zc6uyFymi-J5zwsj)*SK>N9MaNeAMl%n=4_AKGw{50dOP*gpMFphP3P3+hS|3KNc|P@`u!`4lDzRVwcHRr(2V?D9Gcw@d7HBh; zP1UBlBL~wKRGhtubEV*JNndXWk#BCStw)a@j zf>&~uo)6(936&xP7h_#vI6VbO6aM&cXGT1q+eiO*gs?9y41O$h9SHU(Sk=Psw<1>D z=yT+;leQ@yq<$bD!GiggAR{&mjbh0Ke{h(XP|k&>h1PvUy1)o6vx1^jiDxuFPVybH zmKtp_G+ihH=G{73FXK94n{=0b6*t`f-%<*uU`w3#IuNZp?c(qMoj<)GlP#Ee?A4?X z#v+#v7@!((_*)#f5bc45wqJ-o8>2_UT zgGSAuhWoSXQEb(Wy0J9hcTB*DO?x-Rx%{bdt_7Lrhdm%u~5}d5Lo1Z@}8X zx~edK&;;!c@zqha&DG8ZMZ6K8Nr}<)M%pD4s6)T6AHj^y(Akm*%6dN8_=3)|$O~$~ zvqqexf3t&10 z>BaDWUrGQUm2q*gr%mzQr}>*KGKp$vv9%_9P&=92 z#n9^ah{8$AXHSna0x8en{Sp=gREj0U^^#xNB@=sE2X8?9sU9m%Pgt5Z)LZszhUbZd z-77BH%D2UsYfZ{ZC%f>;n7YmhH{N24A4;={jR=^J52coM1gz$UBDU!5t%FC46*LFC zv;%6FqmE|w;gft7@Ta-T9^Edjk$5KB1o~2c($2H?5;q(JPWE`eXdG2i0p%Ocs<>y8 z1~uxXoFLi}8oSVsa+jH82gYUXMHwT>7X2nEka&%Y?h0Gn5;C>(|L#$CcBG~(s;R({ zb_l+%Pl7Sbn-GLW4e1XHEyx!jgi)m=a;oXyWo&wm59;`QYxd-vSYywCNp06N%Sx-e z{ztdk{EMzlOXwa|lj(o{r-PFB^?Z`1r;L{1g>7}%AKlnWYfg`Zls$DNB>1on2>VhsVsSHMRmXy2!mikZ5G1bI{8+3Gu?u*_mYq z$D^V6LW$a&h%VP-4r6E`_F3xDaz1jO8ws^&#)JSgrupJO5Azw_^iNWN+4qeJ0zx=+ zjlQc71z%-+)0_kl+Z=?|jOT^164*kGWxsw;0zariMrmG~rU>*uB`+BSY|DQWeBWr) zty!4oq-dP1Zc;uo7d;+s+IP#_x9baqryRA};2*sdi&StDq%;#Q1}FGnp;w2wFE&Q+ zWq7pO$aRY3Z}%k|vTg)i8{Ej7%DJ-x<7Vy(}+n-u>T`pB>(7oEJ@`nZ_00PE$?KdcAW%F3yzLkQhyPs@gNEV+u z6+dOh+o7o8YtPQ^H?>t8D{kM}tz8ORXbp5Di`WQtqE5^m-;U7r%I3J&w2$w|DEq5r z&}rD4r~XF=HsVqtJ-)VfOkIJ80q^cq20@qHtN`pS2p3j~rLaX^uiU~WA((DAZgF{e z3HItCVJto=={k7p$l6Dtisbo-47l~n-ivnu2hu7AE$2(8qsHLB=v!`sB||b43-F2- zeSdE=b+x$hiKx$Qeo9N;_o;&^u7(6q8H2T0*{JFq_%PhX9vp-h5a^Q=ha)B=X>)?c?)jy~hiB#>+@Fjk58gtt+1@X&JB@zNJU$2-1;D)6-qe0?zMeP} zdjw+jOYs%)el$y@*z;>Na98^lqETv%^%QTc++&@Gyjj*5cOptRsepMGP1rAy-TrWH zaOcgXa7G#ZhNeu#@Hp_XQ{hRQ;i4NSo#pG4RR&Huh0l?&)VJtsKd;lh_&<54FC#?0 zUs6DpD!F?Iv5vdf)6jCMU1?yLepN1)T6eI%D;Nx{k}eZ?+vlSSJzp@(-Yi&Jx!je3 z79Q4*bx9dr9c(4n00JvN=8+`WIMC5eMsqqmykuOz?-s0;MrEC`jAm!x$LD;rJ_D`Pn2hltE{BKK zhMp7~oDWuG?*GEY?9|d)Sk`o6V$@~XFvwp}9>aEV=pCaseEvNK70%1*I=|S>v7M~; ze9d08_HNO7Xz2c}tL5?ui<~Dx7D4D5-Vbln+{jw5GN}~SJMm6mcy*W))ZGmLu_J@0 zwHR2&@egc^n5H@(hwjq*`V959T3e%ta%_B9PgYxw*vWbx0_^bk#j{hl3}=R2fZPNI zx(|u`+$S<2cDY7|M@kF;g^ zBa)4?_sS5;xa%xMBnK)$dRhYUyS+MG)$Xv{&lfv@i;tqUd-i-RO7wI2{jV|Zs#2AO zA=7=nql4QB60Kk_!_soBC&URwInp-|AkLs9Jk_9fAyjc3YIDBX_Zy3fBS}q=d~eq! z)T59n!TB(d0#ljj(3c@G0nH>Uc^mvgKjWcvuGVFm z2fA_8*{8upSXfo?#hR8gxez~TGsBgG26WrEs{y-YMdy!l^!3s3PX!yc#Ce9r=+N5x zwYN{31QOKP>+q(Fx@~M&?}ZbuD@U+x<-}+}%{|L~i$tnS`s>wzbXW?_x`7GtsP916 zS@9n{dutk2rg1s~RMqy5rhxp-tTCpp-6n0i`W5{z{DDzQ5nK^QrF}P8kdOfC{Hs%__wWV1 zZNYAuQ*s>EWL9d>PLArY)NBa0bvvwA< zP^zxRXGmZvt8gu!OD>_LXQ5d|YlT_G9j<&-ouU~Or?eXoPM)InD$NPK0FUPwWp{sH z*hAoAgJd5>S}k;S2R`}nY_i>;Z2I!%fdRrJ3E-5G^;;!OVT(%QQqy1e6{68#W{vTd z#NLxYg7B2^TvAGmxFJS5!hNg=UBH$B`M~4VhjX&FKQM}>3TzxIr)L=CKk5^uD?3A; z2XrQQ0|{6rH4Gy{PYXsLv1{`=3S8k8pmq5R;4^sp`sRx;6{g5um9H4w*%avpklvf( zOm^ij$?S}Pzq5dEp+yvJzxFGYhWqkdmhsQ_1QKkJxJ13!R8Ht88D_7Ds_w6qsg-6+ zhs`d3ynhVa8T$0gs?f_-zyTR4UZUTaPSz0}v1V58!gyAYq%djfRZi|a4xow=+a`3H yHsTPQ@kyuFoqcpW;JD%nv3Vvf^#75^JC;74bedk4^WJ~N6QHK7tyHOK74knLta1PV literal 0 HcmV?d00001 diff --git a/src/pt/taiyo/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/taiyo/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..92ca8345a2f1ee385570ee84f7afe510e43d2b6c GIT binary patch literal 6826 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 img")?.getImageUrl() + title = element.selectFirst("p")!!.text() + } + + override fun popularMangaNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers) + + override fun latestUpdatesSelector() = "main div.grow div.flex:has(div.grow)" + + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { + with(element.selectFirst("a.line-clamp-1")!!) { + setUrlWithoutDomain(attr("href")) + title = text() + } + thumbnail_url = element.selectFirst("img")?.getImageUrl()?.replace("&w=128", "&w=256") + } + + override fun latestUpdatesNextPageSelector() = null + + // =============================== Search =============================== + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler + val id = query.removePrefix(PREFIX_SEARCH) + client.newCall(GET("$baseUrl/media/$id")) + .asObservableSuccess() + .map(::searchMangaByIdParse) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + private fun searchMangaByIdParse(response: Response): MangasPage { + val details = mangaDetailsParse(response.asJsoup()) + return MangasPage(listOf(details), false) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val jsonObj = buildJsonObject { + putJsonObject("0") { + putJsonObject("json") { + put("title", query) + } + } + } + + val requestBody = json.encodeToString(jsonObj).toRequestBody(MEDIA_TYPE) + + return POST("$baseUrl/api/trpc/medias.search?batch=1", headers, requestBody) + } + + override fun searchMangaParse(response: Response): MangasPage { + val obj = response.parseAs>>>().first() + val mangas = obj.data.map { item -> + SManga.create().apply { + url = "/media/${item.id}" + title = item.title + thumbnail_url = item.coverId?.let { + "$baseUrl/_next/image?url=$IMG_CDN/${item.id}/covers/$it.jpg&w=256&q=75" + } + } + } + return MangasPage(mangas, false) + } + + override fun searchMangaSelector(): String { + throw UnsupportedOperationException() + } + + override fun searchMangaFromElement(element: Element): SManga { + throw UnsupportedOperationException() + } + + override fun searchMangaNextPageSelector(): String? { + throw UnsupportedOperationException() + } + + // =========================== Manga Details ============================ + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + setUrlWithoutDomain(document.location()) + thumbnail_url = document.selectFirst("section:has(h2) img")?.getImageUrl() + title = document.selectFirst("p.media-title")!!.text() + + val additionalDataObj = document.parseJsonFromDocument { + substringBefore(",\\\"trackers\\\"") + "}" + } + + genre = additionalDataObj?.genres?.joinToString { it.portugueseName } + status = when (additionalDataObj?.status.orEmpty()) { + "FINISHED" -> SManga.COMPLETED + "RELEASING" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + + description = buildString { + val synopsis = document.selectFirst("section > div.flex + div p")?.text() + ?: additionalDataObj?.synopsis + synopsis?.also { append("$it\n\n") } + + additionalDataObj?.titles?.takeIf { it.isNotEmpty() }?.run { + append("Títulos alternativos:") + forEach { + val languageName = Locale(it.language.substringBefore("_")).displayLanguage + append("\n\t$languageName: ${it.title}") + } + } + } + } + + // ============================== Chapters ============================== + override fun fetchChapterList(manga: SManga): Observable> { + val id = manga.url.substringAfter("/media/").trimEnd('/') + var page = 1 + val apiUrl = "$baseUrl/api/trpc/mediaChapters.getByMediaId?batch=1".toHttpUrl() + val chapters = buildList { + do { + val input = buildJsonObject { + putJsonObject("0") { + putJsonObject("json") { + put("mediaId", id) + put("page", page) + put("perPage", 50) + } + } + } + + page++ + + val pageUrl = apiUrl.newBuilder() + .addQueryParameter("input", json.encodeToString(input)) + .build() + + val res = client.newCall(GET(pageUrl, headers)).execute() + val parsed = res.parseAs>>().first().data + addAll( + parsed.chapters.map { + SChapter.create().apply { + chapter_number = it.number + name = it.title ?: "Capítulo ${it.number}".replace(".0", "") + url = "/chapter/${it.id}/1" + date_upload = it.createdAt.orEmpty().toDate() + } + }, + ) + } while (page <= parsed.totalPages) + } + + return Observable.just(chapters.sortedByDescending { it.chapter_number }) + } + + override fun chapterListSelector(): String { + throw UnsupportedOperationException() + } + + override fun chapterFromElement(element: Element): SChapter { + throw UnsupportedOperationException() + } + + // =============================== Pages ================================ + override fun pageListParse(document: Document): List { + val chapterObj = document.parseJsonFromDocument("mediaChapter") { + substringBefore(",\\\"chapters\\\"") + "}}" + }!! + + val base = "$IMG_CDN/${chapterObj.media.id}/chapters/${chapterObj.id}" + + return chapterObj.pages.mapIndexed { index, item -> + Page(index, imageUrl = "$base/${item.id}.jpg") + } + } + + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException() + } + + // ============================= Utilities ============================== + private fun Element.getImageUrl() = absUrl("srcset").substringBefore(" ") + + private inline fun Response.parseAs(): T = use { + json.decodeFromStream(it.body.byteStream()) + } + + private fun String.toDate(): Long { + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L + } + + private inline fun Document.parseJsonFromDocument( + itemName: String = "media", + crossinline transformer: String.() -> String, + ): T? { + return runCatching { + val script = selectFirst("script:containsData($itemName\\\\\":):containsData(\\\"6:\\[)")!!.data() + val obj = script.substringAfter(",{\\\"$itemName\\\":") + .run(transformer) + .replace("\\", "") + json.decodeFromString(obj) + }.onFailure { it.printStackTrace() }.getOrNull() + } + + companion object { + const val PREFIX_SEARCH = "id:" + + private const val IMG_CDN = "https://cdn.taiyo.moe/medias" + + private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ENGLISH) + } + } +} diff --git a/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/TaiyoUrlActivity.kt b/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/TaiyoUrlActivity.kt new file mode 100644 index 000000000..68764df25 --- /dev/null +++ b/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/TaiyoUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.extension.pt.taiyo + +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://www.taiyo.moe/media/ intents + * and redirects them to the main Tachiyomi process. + */ +class TaiyoUrlActivity : Activity() { + + private val tag = javaClass.simpleName + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val item = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Taiyo.PREFIX_SEARCH}$item") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e(tag, e.toString()) + } + } else { + Log.e(tag, "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/dto/TaiyoDto.kt b/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/dto/TaiyoDto.kt new file mode 100644 index 000000000..d92017129 --- /dev/null +++ b/src/pt/taiyo/src/eu/kanade/tachiyomi/extension/pt/taiyo/dto/TaiyoDto.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.extension.pt.taiyo.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseDto(val result: ResultDto) { + val data: T = result.data.json +} + +@Serializable +data class ResultDto(val data: DataDto) + +@Serializable +data class DataDto(val json: T) + +@Serializable +data class SearchResultDto( + val id: String, + val title: String, + val coverId: String? = null, +) + +@Serializable +data class AdditionalInfoDto( + val synopsis: String? = null, + val status: String? = null, + val genres: List? = null, + val titles: List? = null, +) + +enum class Genre(val portugueseName: String) { + ACTION("Ação"), + ADVENTURE("Aventura"), + COMEDY("Comédia"), + DRAMA("Drama"), + ECCHI("Ecchi"), + FANTASY("Fantasia"), + HENTAI("Hentai"), + HORROR("Horror"), + MAHOU_SHOUJO("Mahou Shoujo"), + MECHA("Mecha"), + MUSIC("Música"), + MYSTERY("Mistério"), + PSYCHOLOGICAL("Psicológico"), + ROMANCE("Romance"), + SCI_FI("Sci-fi"), + SLICE_OF_LIFE("Slice of Life"), + SPORTS("Esportes"), + SUPERNATURAL("Sobrenatural"), + THRILLER("Thriller"), +} + +@Serializable +data class TitleDto(val title: String, val language: String) + +@Serializable +data class ChapterListDto(val chapters: List, val totalPages: Int) + +@Serializable +data class ChapterDto( + val id: String, + val number: Float, + val scans: List, + val title: String?, + val createdAt: String? = null, +) + +@Serializable +data class ScanDto(val name: String) + +@Serializable +data class MediaChapterDto( + val id: String, + val media: ItemId, + val pages: List, +) + +@Serializable +data class ItemId(val id: String)