From 5f905c713db67e12987d23ee694605b1d5829b9a Mon Sep 17 00:00:00 2001 From: zhongfly <11155705+zhongfly@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:30:43 +0800 Subject: [PATCH] Add Zaimanhua (#5092) --- src/zh/zaimanhua/build.gradle | 7 + .../zaimanhua/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2392 bytes .../zaimanhua/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1429 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2759 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4321 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6096 bytes .../extension/zh/zaimanhua/Common.kt | 30 +++ .../extension/zh/zaimanhua/Zaimanhua.kt | 225 ++++++++++++++++++ .../extension/zh/zaimanhua/ZaimanhuaDto.kt | 144 +++++++++++ 9 files changed, 406 insertions(+) create mode 100644 src/zh/zaimanhua/build.gradle create mode 100644 src/zh/zaimanhua/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/zh/zaimanhua/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/zh/zaimanhua/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/zh/zaimanhua/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/zh/zaimanhua/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Common.kt create mode 100644 src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt create mode 100644 src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/ZaimanhuaDto.kt diff --git a/src/zh/zaimanhua/build.gradle b/src/zh/zaimanhua/build.gradle new file mode 100644 index 000000000..fc35f92bd --- /dev/null +++ b/src/zh/zaimanhua/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = 'Zaimanhua' + extClass = '.Zaimanhua' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/zaimanhua/res/mipmap-hdpi/ic_launcher.png b/src/zh/zaimanhua/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f09cf1fbdcc35a1461df18863e00ca5addff551b GIT binary patch literal 2392 zcmV-e38(gnP)IMyl)x7!ZUOqy?;KO94R& z0^R7orA^W{w53fqXw$S!NYjum@cVzKy?T3+G)tQ{2{|+0+$QJTbMOD<{M-3Y@3|+| zVlCEUE!HBt5z#cp^eDqLScL=zB`*y@*M3yYO-9A$-tKW@O`pFYzW5S`Kp}}YZWMYD@u!zb+ z@ZTo!;OV%!T&@=ta^znj(!p{KI-O3cs;XM3mf5d1GI*N9;n)_f1Th8&1_sDzH13A1 z!4Pz>&1Tz=$)b@U$ixi<9fqvI5H#Ilu^dn)lONy@I&Ds>Znl!y>ZJaGA-Dc1h9F3< z*C#^OUod^XSV%<+SfyEv1*Vk@i|QwOJhE9(kS+ zLJ*|W>5f3wNhiqZ8l>G!(DGe{^y;=edSy$l8&LqhU-45ReVR~4-yb(nQbq&i*L2d2 z?mp_fH6T})Yz`MyH1^Qx!d5}pw?`}Ktvy9v2~)6eYc73spp>DsnT_;avYu9I3Tg3=`GS-M9>`j- z`3f!DSwJ7|FQs+IbhJOcjxu%aWHR@X-8mrU<>=4tc|H(+1bG8i#no0DoiDpi`_I=2 z(%#!wLW@{6WwO+Gd2=qU+FMMU{;Z-ua+>H$m06H)bM*V8mXSk{8t`mA?B$?NW%b>3 zBClC^z*T!oXvvOzK^j(+Ocqw&;_dl@*wu+zq0ax5-w^Z3&lo{u666g~BaoI^*-nX< z8))O72KpqvOn5|@ECuV1Rg!DKt7nWZg4BS$vG%&CrPDz=QxBz=UZ-7b%t6?)T?K+P zsNbQB*J$u=SwS*jZ}O@;N2w6VXtVTD2Cp&%r?|@ZsSrK{VVOfkCJH(Z8xWx*eoEsI zpqD)vo&$mq4nX}r<`BVPvbw#C3gJNz1Ug&XMh7p{`$YWtdfIcgRv1`B*>XQ{fX85U zEa9-=b;b$~L=i3^plt7P(!1={mU2jdZ~!5N{5~p#2SKnGtz8bPWB2O|S^5sKDqvtY zvg?-ny`h!U6g&qjwXwaIuHCRx8$WXl`vZgrL4ly21zWkhND#E=>{Yk^2}bx4gn+OK z(QjYB$H>byejc>10|`T`Rl64$OtSx!4l%Bj@sl=q700QJqIS764)d z`9OFR6!*KHKlhD7q0vHbjKo18Q0_;3vr;omVAKDBUREO(a396D#O=c9XD48~!kmdF;&H6DFS^Y+hYwZ*x4yB#i72{NmWv+kUb} z0H2je_-(c&%J&M8!6Sl3x#ydt3b%e3aYWF#A61U84{OC(q#wU!HjcIb>$JoxPENPql$sb zsiMx?(rVns2Z+!Tget=3)GBdW<48uMsCc6Ci7JLXRzaWyDS#b|zfkv|Dx>_I&8tZs z7a&4Q5cZef)C_0dsA9q}BQ|-T-T1b&YD&v%CN0|{Y>UE}L&y&HcsK*}?U72Mp7NYy z0qVAfgF_^&rFC888vl{L&=Q1F3y4vERZm#NZXbsU5Dy(VUq|Ok+9X(%ukT5m2@FS$&r7DDZm52gV%5K4j!D>d9Fqk&a*<1bt&1 zGzbMkOOOnx90Y|YOU-Sj9j9spVZKh1p&x0-S|%9z*XhC*QCdbKI)SUj6DF4+HAIz@ zjWtvPsYZ^WgAQMA5C@|Waig^JvKQh&7(xeBSl4hmfm{x!$q;iP0d~43OwFc_AaB6I zP8?Z*kwv|JH?Nq_50?LzfO-Se^9^@xt$tt4si82afamhR6$EH2i3!yAa8)7Ztv+62cs^s14y_~FEV$KArqw_ z;h*}uhafdzYv3(m&k##t&-x^<7N{A{A->DeHHf=hG3G%jZ-{(?yaA*^EsZTU%CGLE z_U=BnF@9h|2nzatH6j2CK}nD`D1s~&OIWU;i2!5*K?Z{%_!ZRA(V>aX)y$QTzF=f8TX+uXTxAK=RqjAu0(&D!+z^yKX9?6i!GjMS{GtP^TvW@e81TmJ0t zm{80)a!tj%Lk7qKnIPMoIddKyEp=dR+U(i0AL8p{jHeh+GoFcXKnBS2=&V_@9_DLc zs^zIMN%!+=n!(qZJn18m49GAdi~mMBfCjds0kIZqu@-9)(f$QjUBoBr6_DKk0000< KMNUMnLSTYv8+60~ literal 0 HcmV?d00001 diff --git a/src/zh/zaimanhua/res/mipmap-mdpi/ic_launcher.png b/src/zh/zaimanhua/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..626a6b583a6f3e6ead6cee7db584625736d9c85f GIT binary patch literal 1429 zcmV;G1#0?u*zK7)OUjB=HM=F;NpuG)D9fSoTr((L|JaiJ2Pbg&2)@yoAIUgb6C*CIj3+HfC|i zq+?tLC>!l)*}86wjc)A*yWe^}?Y6F`?e_RQho0{AJ*Vkbx}NiaC;8>H@9q44@9lZs z_pDlV+cazdkb_vQGICg6DGe*#Nnq2}vSsm+7R2A{5Dz0BQVI0#bxPb6O)P-Vdl6*| z3k#F;^Yb1x5#vxsoTMBTP0Wl&qx-NTU`5S<^UdlDW7y(Nf0d9te7q%PW`E3T+SZst( zDy&d;+5!J|Ou*38G(@6NS)JuVDxj*W>M1GL1WQN2lrIRo>w4k+y_UETDPH^81mEhe z!k>);&}w&scRD0%BNbLufD|+u7Ka;-G!4LaN4lVJmq{jQ!*&B~Dr<#Z`W~on8HL^v zKZL>&SzA(MO#ln53L~x=xX?KP`_A;iX9wD0;}7ytdTB>9Y(CTh73cclV%HQ*cxHvV zi|wc+KncN61iFWOpf?Z0&RQG1Q`!O>7(s-?7jDGsXi5N!IX(g|ZVpU6PN+oETdF(Z z)gLdx#qOzi8O;jt212lDPb-eb2t;FP9oIeZB~B-rMq;_F2sqX}1c&~+u86IFb-`Da z9dO`$e?mGfsc_ru*xEWk)y>nMcy z#Ko6Nz@#?_*YGSbFgXpUus*e=Ys#X4^SFQS$8aDRQoemiC4fe-e>?zN4tK^!@w8=B zR*pr+1nfCw7hX#7%hsAMs4)yed%p+#vwU0t9Y!eTVM$0qYlfU1m7^0Ho6jO+0%$iO zf@1H9pxcNK(A$>O_hOHZ3BAmRZ~Zz&M8*UVsF!a{1mLW797<2xg)l+sI^`7<`c2gp zA@Zcd%2LASWXPxhB}`*%Pe;MOjYGm|>doDYJCzi)Ywb984UU_Jmvxi3;sRJ;6&S}e zEnSh&Rmo;t)a3A3L3G9PX@wQ`;?(RN;biSBwwGlAN}za$NYZ5%orURgi@dfdrs?XW ztd59u1uXe;H(k&*BRMp3W)xGg`68qOmUOvmGMS#p^a$Jvqynm|tDlx~O)!XkG^nku zeJ~sjyQxxCBOf@B8+g55PjPYaqr|bXu~9C73~qkDxyRvf97PdS4@ihwCcc~Jd1z~E z`vShEK~*b?q|`63vo(GUNkCLP*89;{#=W=Pi3qnE=wjaHCik< j_wG^?Nf+#G(}?{K*Z&SRd^t{V00000NkvXXu0mjfl`oS_ literal 0 HcmV?d00001 diff --git a/src/zh/zaimanhua/res/mipmap-xhdpi/ic_launcher.png b/src/zh/zaimanhua/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b119042fdb62852cbda22dd97d9d49531bff100d GIT binary patch literal 2759 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 N002ovPDHLkV1iKPQl9_- literal 0 HcmV?d00001 diff --git a/src/zh/zaimanhua/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/zaimanhua/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c18528d0dc650041be831a56fd112d4766cd2c GIT binary patch literal 4321 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 literal 0 HcmV?d00001 diff --git a/src/zh/zaimanhua/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/zaimanhua/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..0495adc33632679660b948ef8074a46dbfe94e0a GIT binary patch literal 6096 zcmb7IcTiJZlz$-*Y5=8#4kA@RgA}DnKza#9L7EVX7zF{Tp-HGx73rPOK`B8|ij+`9 zL4?qYbWn;&Q+mnf`*vsckDb|_y)*CKnS0LMQ|~#ydt;1|+E6+!IsgEmx;h%Bp|t{Jw)*=Ha{nm3m1@}u3?Pv{&b1tTJP;Q0`2P<|M!_thhP!Y}cw~$V z4urN(vK5tPYe4*V*qd9LM_}TCgsvByvI{4N2icVMw410!F7&(z|8exQ%hB`=l$f(K z>acoB&M?LHM1r^n{3-3`8NBg5V2I#W-7d>7r`y%@>B`??D^AY88FH@7Qg#^Q-QLn# z$I7-4+_BQ1tKWP$g}!TZSbpdi1)@26xY}VY#`t{6%r47Radk0;+HflJ*992H)<#U) z01#j)*52I;4i0`3nc`$eu(sBNG%7XU?9p9m#q1X4hIj74df}l67msI7P7z$(+%Uu) zBD-wqcQ?v3{FNZjxZOe2n^kj)-(p;GS8k%hyz2l#fDmK9bWiqJVl$VR{5QP>rbP0P<` zx?Ik-wzdW*lpxEz60}J<$M8}>>Jqf~+qW5-gp!Z%OOco?A@65p4K~%T1H<`HLIwo` zTvk>?KD$Z>y`fLEa&RbAysW;KR~DkB)qSsv_QrhtK;vg$D6A7+5ca$RgE!UH)%7wp z9jeE@UtV_d8Nx|fe=uOPD~)D3t-d*VK^}0to*`7UzP|n{B_$;!Y|Omc#y`{cdD^Eu zl|YZ{cf343TeUZj8M$~hnS&^E66%ro}<&z>V}XNl_b z+N9qudad8{Sn^G1FLywNc1A}}DV~xa(lF!*2pu?2j#|YQjt{@K1KRdNDFMoH6t98p&M* zGXdBO`sdFGILKtyZd0n%3TE5R8f*Mw$}MFn`8_hB@P&7S*&ZAL8ebJ_!S_2eKe?%9 zx406AB<9F9{{6XcI`zuL=+nc1a=W^VH~G&LuDg*p1d0?%XTE%A%LzMspgxLH4CX21 zZ2wF>Akc`7m-Uh5lkAV--zt*1x?_@1^Hw9SV`^NlAVX@Jj0<$L`Jp zOWA8B(PMn&rH*A&`(|nNvtnfTuV|48AU52g>1VZCub4}EXKszFWMQV@bN(%%>`Ywv z+4RSwx+_(tbq?R0+}DfFFFoG?uhrZ(n*dB8yPm8g1yM9C4qxv`td6TkFwsh{Q*k{a z=KDVujwK<7x;y1k%m&q@X2SLS@RNWE<0ijpS%0hj+uY5h5|)}^8^K8Q9$fPHx=yY$ zIUS05AEac>AWD2x^q`$@K@P0(ilg;R#Dzt3OQ#$6yz%_^v&5gPk$onc@u^#5;p54M zcX9EaI`wzDJ9y9{Wr_|8{U-%OvDNn&_h~WaRCQ~)N3@Qv6VGw+yi-*8EtN$1W|EYZ zP?Y^7@RWgaY)D1J@&|8MQ*v`MP z7V>}>2n{N_X=WH)*M_rvv5AXctSzxjD?@JjuuT5#?M_ayoOQc-u+srSQCwdnOm{J} z#nw6f+OZ07w64fu!h3SC$3^{JQLvo=48EY;C`!gZ>rJh;ocU(e7wA+!j*AfA4jGB* zZ#mEDrsu|TCfAC5NK8M-@iDe+`FQ9)`H;YMo@9$iOTJ_ zQ>N-jJFYWGdqCeoPl;iX}8CdvyYc?G0di!UR(-uP?{ z>J!W(?kXir*0X~JUCXi15n7qas+rqSOCl89SZSy?66UHPoIjiKXmgcY{?wp6hu4$1 zIjz-*r1-((>@L>>K^cG)r-JJC3xm!lmxC-%OG%R`guX{%Wv;D_`$yxE`p14H6}%|2 zdIk#-I+n(aRRiDok-a$C9ZIGT5uhKO7AM%;-R3uzCVZj0?q>r=tnz@2J$|@r>FYa& z#c^!noZg$n2kgg=asok>{DfQDty+)cw%HC$cwjrn6L);0Sw=-E@2NGP)!{Pyd%+8s!Q&CIm>;S7r9+Sx!r_WloxMbFtEXfV$$t#OGd$n&viu|NDGM^i38N^-*^7ht-X&)OA1XVT6DNVEXs z;DxTM1Ft9~s`9MXLgIW!CV#!86}4Dibmw3(@^cm6VmjUY`#A%|Ef5vzf#JBDa2rxk)$}tRBDI`hS%|9H> zAGCX;M`pQ{2&%KfG@FXE;)O0`_phrOEqR#4Eo7j?Dt^;iG%*+tlX|0)1)nIoG?=H7 z1$S6L%3bL1>nyBOe%Dkb`# z0gMqK#+33{IkL!5C<^+&lW3fP6t+s8szjN}g2d(lB-6EhQdS|kvG#fxz$IraH|x8j zp(r;zsE|P(+t?1lmTS;s&-TYRD(GVBhpvD>0Hpeg2{LbUV@rxPmWGOZ-e(gW!9orW z9#k1hj-@N$LN=AM^+xzjyn zN`?YEGn;+?Mc8<}qH#LlE0ciLA*Jx@H(Rvrml8z)C!!q4omimIcn z$a6GbO*YTsl_6WgST+_4SDOlaj_xer%6Hd$4k8z{Z$(gi265eL?Gp=vN>PzJSefD4LyEAm%f|HE`?FU%rOj&t{LBdK0-DOqe?PCb%dfZE zX7E0m04{~yC^=dN5gM)3Xc4?)Oz%9&@tui{E6zrbHrLiuQv{tc?{u+8epLMH#L=c#NFhk`w%E^ zF+=Y((JMjF|6FeVIN;Wh831Pj7K-{NB^O*Yl0mPtj*E$VE3^~9Pc03Gc`n`(e{s#I z>@}z!zpt~LaZOLhIKCGscY3nHgDdS<^MBGW5Ig*~SLw7rfm|Ru$NoJZY6FDmfkJuZ zorhNkk|M1aF1ALQL9jwTDhS+2<=AF{u!uj1Yr~#fUO+F~9x2pxzbfO*f^CP^=vC#3f zLou62(D0gDiPa_bc#nC%Qc_M#;cE~=_sKv;1#>H@-!{;=NaxCh0|)|?bMAZ$Ru6jo z-~|t-8nwdQtS-5S?}}HDrj2Qxu$+P_E*p3^iB}63$l3lovTaneSKRI6uW^?yRpSt}ZTV=5 zjE^ibi>G|*j-3nf9`bM$r5LKvPP)F#p~&7;B-Es)+(NBQQHGq*r}oigW^>NoIW9Dk z`E~gg*AJ$@al~DFK9rGdGHVN`RQ_n!dyp~c*a@r4`wp=lQDrhaZGJ^F;f0`9b~9B3 zr)7qdan2oqBKXgX;}ArduQD5sg{q2jD7TTHHTH#Ls*S2gQ%Qb5BbA#l$DbG%Fv^MQ zT^>`6a$u#&3lH!e)V_jA@hy(PfvSzf?~;Y74tFA6Hpl~5f-wYZ6Fk#CTxeu03F|)O z;z|&L3$1(NI~aI3>ZY|6cqT{r8yjOV0-I1A^`iRok=c=J+CF&E75BMo#rfA1?foS} zO+>wu2HU$U<$j9v*X8{%cc8$7!hmEYu~-SbU(Zem18u);s=f^S@oXa_HyEIQy3|7dtf; zGr9eq3nFJea93cMuysMO)7r%NeiKGJvr<|;p=Gp!t1<+i@sf^ie%LxD9i$Pj#* z{S;RfnRGDqfcoT>kRz=|@n9_J+^;1e>0olr%lchTHsN^IViWZaBnfE?xm&zQ8()&{ z?kbUL-;$25$YVK}=Tn06w$wBi;GGC<~Li3mUtA4U*T+`S4r)BdD^4tPi zgy1d*-#&(-21edV*!bjXf#%qUi-@%7z*v$ovTCT?(iO`AIbdfiEx*)JYOC*;Htf3N z)J=Ewjp78yBU5n|2XGf>!(0Ys=ce1lITi1Q=sjm`NuT4~Rf*Ms9=!?RDdVPUnxlCv z<9=(Hv(>wgdcJsY3o1}>e)R3J7ycso0uHd%uGUQ<{Ra2r6&rm7&Fc(heLCP*oImZ6 z@>B7k3>@M!o5ZSdSc0kR2wQ>dNjvjT5o5{dzxt#}b294hri!wc2ANxpUxxjXJgKyC zi{n7U6eKrIp8O)CR7Wnc;?8M}yq08n{;CqyAR9r^9@#sRla^Occ#fCI4VVxCMbkgv z6+2gQaw#a}+WTPW1cpC_a${p$$TtR?P6%o-3oV0G9=O{72y5I#kPFzJr%m??mJh}w` zI{C>Wm9MwC=Fd;I+N^?d^Rlvz%R3hkm$AvJHEjY>w6FePaO`YsgN2%L&!`e+{N{qk z5ng=1E&0B5ip*h_5_CeVVL^L+nKDP0FYo>sMc$nFtSQ}>fyq1X-{1GW$TVhJP}LUE zU22s7OjvehFz-iba>HGZPq}`x{^5rK`ktP8JT3;z~Apz za8=*k+s?Gm--;AU7(9+f@7bM+j*}@;ivxCZErEWqXj&Z`f`10qnnJ`vb|>zpov|^( zWxRTZ!cfhc$WixmFy|r!Apq;tA*R=~ogb^R*|mu@1gA&$_Udu*@ueAZUZsJ3c1Hch ztWhYmv7G5dhnyYnQR9Ts>rKC^J875|2}Ca0vJ_CMP{VWe8rT_#W4?yc&uyyik(a&w zCAk|nzNXOhk0CHt)FQ~i4t?0j)Rf-mm;9oy^xRpvn&_=|(R9HEXa=i0h2Y9hk+M42 zL0qo@+@rr$E3S8UI>^BFap%aMOF#oNH#KG&D88Zz6Rz|BM> zGr`1vYg(LPLF~#EuJ@C!S4FOJvT=FvsRY!?$jOc7B_-`1eZAIp8cXb*Qptz#3H7*cust5;Zj`5d6v(8!O)zpE$Ww9Y&rukF z@5Dqa5P`aX9!ZHbbrjzaO2kBMeIje!zk4Z&CeFK+F86taolv;tD*CKH87?zwvj$kE zt1?6FOB>z`ya(af(A`D6Xd0`q(iF~<1K3Rk1(}@fNLu34_AkVHRCchSJ2vyY<2GL3 zW(4a`_P^bjJLPTn%WPOyB4UXh_lcBs3xoeDd3N#MZq{)0KV95lmGw# literal 0 HcmV?d00001 diff --git a/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Common.kt b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Common.kt new file mode 100644 index 000000000..64e93963f --- /dev/null +++ b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Common.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.extension.zh.zaimanhua + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +val json: Json by injectLazy() + +inline fun Response.parseAs(): T { + return json.decodeFromString(body.string()) +} + +fun parseStatus(status: String): Int = when (status) { + "连载中" -> SManga.ONGOING + "已完结" -> SManga.COMPLETED + else -> SManga.UNKNOWN +} + +private val chapterNameRegex = Regex("""(?:连载版?)?(\d[.\d]*)([话卷])?""") + +fun String.formatChapterName(): String { + val match = chapterNameRegex.matchEntire(this) ?: return this + val (number, optionalType) = match.destructured + val type = optionalType.ifEmpty { "话" } + return "第$number$type" +} + +fun String.formatList() = replace("/", ", ") diff --git a/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt new file mode 100644 index 000000000..d95c46ff3 --- /dev/null +++ b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt @@ -0,0 +1,225 @@ +package eu.kanade.tachiyomi.extension.zh.zaimanhua + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +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 okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.security.MessageDigest + +class Zaimanhua : HttpSource(), ConfigurableSource { + override val lang = "zh" + override val supportsLatest = true + + override val name = "再漫画" + override val baseUrl = "https://manhua.zaimanhua.com" + private val apiUrl = "https://v4api.zaimanhua.com/app/v1" + private val accountApiUrl = "https://account-api.zaimanhua.com/v1" + + private val preferences: SharedPreferences = + Injekt.get().getSharedPreferences("source_$id", 0x0000) + + override val client: OkHttpClient = + network.client.newBuilder().rateLimit(5).addInterceptor(::authIntercept).build() + + private fun authIntercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (request.url.host != "v4api.zaimanhua.com" || !request.headers["authorization"].isNullOrBlank()) { + return chain.proceed(request) + } + + var token: String = preferences.getString("TOKEN", "")!! + if (token.isBlank() || !isValid(token)) { + val username = preferences.getString("USERNAME", "")!! + val password = preferences.getString("PASSWORD", "")!! + token = getToken(username, password) + if (token.isBlank()) { + preferences.edit().putString("TOKEN", "").apply() + preferences.edit().putString("USERNAME", "").apply() + preferences.edit().putString("PASSWORD", "").apply() + return chain.proceed(request) + } else { + preferences.edit().putString("TOKEN", token).apply() + apiHeaders = apiHeaders.newBuilder().setToken(token).build() + } + } + val authRequest = request.newBuilder().apply { + header("authorization", "Bearer $token") + }.build() + return chain.proceed(authRequest) + } + + private fun Headers.Builder.setToken(token: String): Headers.Builder = apply { + if (token.isNotBlank()) set("authorization", "Bearer $token") + } + + private var apiHeaders = headersBuilder().setToken(preferences.getString("TOKEN", "")!!).build() + + private fun isValid(token: String): Boolean { + val response = client.newCall( + GET( + "$accountApiUrl/userInfo/get", + headersBuilder().setToken(token).build(), + ), + ).execute().parseAs>() + return response.errno == 0 + } + + private fun getToken(username: String, password: String): String { + if (username.isBlank() || password.isBlank()) return "" + val passwordEncoded = + MessageDigest.getInstance("MD5").digest(password.toByteArray(Charsets.UTF_8)) + .joinToString("") { "%02x".format(it) } + val formBody: RequestBody = FormBody.Builder().addEncoded("username", username) + .addEncoded("passwd", passwordEncoded).build() + val response = client.newCall( + POST( + "$accountApiUrl/login/passwd", + headers, + formBody, + ), + ).execute().parseAs>() + return response.data.user?.token ?: "" + } + + // Detail + // path: "/comic/detail/mangaId" + override fun mangaDetailsRequest(manga: SManga): Request = + GET("$apiUrl/comic/detail/${manga.url}", apiHeaders) + + override fun mangaDetailsParse(response: Response): SManga { + val result = response.parseAs>>() + if (result.errmsg.isNotBlank()) { + throw Exception(result.errmsg) + } else { + return result.data.data!!.toSManga() + } + } + + // Chapter + override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + val result = response.parseAs>>() + if (result.errmsg.isNotBlank()) { + throw Exception(result.errmsg) + } else { + return result.data.data!!.parseChapterList() + } + } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + // PageList + // path: "/comic/chapter/mangaId/chapterId" + override fun pageListRequest(chapter: SChapter) = + GET("$apiUrl/comic/chapter/${chapter.url}", apiHeaders) + + override fun pageListParse(response: Response): List { + val result = response.parseAs>>() + if (result.errmsg.isNotBlank()) { + throw Exception(result.errmsg) + } else { + return result.data.data!!.images.mapIndexed { index, it -> + Page(index, imageUrl = it) + } + } + } + + // Popular + private fun rankApiUrl(): HttpUrl.Builder = + "$apiUrl/comic/rank/list".toHttpUrl().newBuilder().addQueryParameter("by_time", "3") + .addQueryParameter("tag_id", "0").addQueryParameter("rank_type", "0") + + override fun popularMangaRequest(page: Int): Request = GET( + rankApiUrl().apply { + addQueryParameter("page", page.toString()) + }.build(), + apiHeaders, + ) + + override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response) + + // Search + private fun searchApiUrl(): HttpUrl.Builder = + "$apiUrl/search/index".toHttpUrl().newBuilder().addQueryParameter("source", "0") + .addQueryParameter("size", "20") + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET( + searchApiUrl().apply { + addQueryParameter("keyword", query) + addQueryParameter("page", page.toString()) + }.build(), + apiHeaders, + ) + + override fun searchMangaParse(response: Response): MangasPage = + response.parseAs>().data.toMangasPage() + + // Latest + // "$apiUrl/comic/update/list/1/$page" is same content + override fun latestUpdatesRequest(page: Int): Request = + GET("$apiUrl/comic/update/list/0/$page", apiHeaders) + + override fun latestUpdatesParse(response: Response): MangasPage { + val mangas = response.parseAs>>().data + return MangasPage(mangas.map { it.toSManga() }, true) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + EditTextPreference(screen.context).apply { + key = "USERNAME" + title = "用户名" + summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置" + setOnPreferenceChangeListener { _, _ -> + // clean token after username/password changed + preferences.edit().putString("TOKEN", "").apply() + true + } + }.let(screen::addPreference) + + EditTextPreference(screen.context).apply { + key = "PASSWORD" + title = "密码" + summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置" + setOnPreferenceChangeListener { _, _ -> + // clean token after username/password changed + preferences.edit().putString("TOKEN", "").apply() + true + } + }.let(screen::addPreference) + + EditTextPreference(screen.context).apply { + key = "TOKEN" + title = "令牌(Token)" + summary = "当前登录状态:${ + if (preferences.getString("TOKEN", "").isNullOrEmpty()) "未登录" else "已登录" + }\n填写用户名和密码后,不会立刻尝试登录,会在下次请求时自动尝试" + + setEnabled(false) + }.let(screen::addPreference) + } + } +} diff --git a/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/ZaimanhuaDto.kt b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/ZaimanhuaDto.kt new file mode 100644 index 000000000..cf4e8c334 --- /dev/null +++ b/src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/ZaimanhuaDto.kt @@ -0,0 +1,144 @@ +package eu.kanade.tachiyomi.extension.zh.zaimanhua + +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +@Serializable +class MangaDto( + private val id: Int, + private val title: String, + private val cover: String, + private val description: String? = null, + private val types: List, + private val status: List, + private val authors: List, + @SerialName("chapters") + private val chapterGroups: List, +) { + fun toSManga() = SManga.create().apply { + url = id.toString() + title = this@MangaDto.title + author = authors.joinToString { it.name } + description = this@MangaDto.description + genre = types.joinToString { it.name } + status = parseStatus(this@MangaDto.status[0].name) + thumbnail_url = cover + initialized = true + } + + fun parseChapterList(): List { + val mangaId = id.toString() + val size = chapterGroups.sumOf { it.size } + return chapterGroups.flatMapTo(ArrayList(size)) { + it.toSChapterList(mangaId) + } + } +} + +@Serializable +class ChapterGroupDto( + private val title: String, + private val data: List, +) { + fun toSChapterList(mangaId: String): List { + val groupName = title + val isDefaultGroup = groupName == "连载" + return data.map { + it.toSChapterInternal().apply { + url = "$mangaId/$url" + if (!isDefaultGroup) scanlator = groupName + } + } + } + + val size get() = data.size +} + +@Serializable +class ChapterDto( + @SerialName("chapter_id") + private val id: Int, + @SerialName("chapter_title") + private val name: String, + @SerialName("updatetime") + private val updateTime: Long = 0, +) { + fun toSChapterInternal() = SChapter.create().apply { + url = id.toString() + name = this@ChapterDto.name.formatChapterName() + date_upload = updateTime * 1000 + } +} + +@Serializable +class ChapterImagesDto( + @SerialName("page_url_hd") + val images: List, +) + +@Serializable +class PageDto( + private val list: List?, + private val page: Int, + private val size: Int, + private val total: Int, +) { + fun toMangasPage(): MangasPage { + if (list.isNullOrEmpty()) throw Exception("漫画结果为空,请检查输入") + val hasNextPage = page * size < total + return MangasPage(list.map { it.toSManga() }, hasNextPage) + } +} + +@Serializable +class PageItemDto( + @JsonNames("comic_id") + private val id: Int, + private val title: String, + private val authors: String = "", + private val status: String, + private val cover: String, + private val types: String, +) { + fun toSManga() = SManga.create().apply { + url = this@PageItemDto.id.toString() + title = this@PageItemDto.title + author = authors.formatList() + genre = types.formatList() + status = parseStatus(this@PageItemDto.status) + thumbnail_url = cover + } +} + +@Serializable +class TagDto( + @SerialName("tag_name") + val name: String, +) + +@Serializable +class UserDto( + @JsonNames("userInfo") + val user: UserInfoDto?, +) { + @Serializable + class UserInfoDto( + val token: String, + ) +} + +@Serializable +class DataWrapperDto( + val data: T?, +) + +@Serializable +class ResponseDto( + val errno: Int = 0, + val errmsg: String = "", + val data: T, +)