From a9bd85d728f50fed90c5bf0950c9d7b43cd65f4b Mon Sep 17 00:00:00 2001 From: kasperskier <95685115+kasperskier@users.noreply.github.com> Date: Fri, 17 Jun 2022 09:59:58 +0800 Subject: [PATCH] Add GANMA! (#12206) --- src/ja/ganma/AndroidManifest.xml | 2 + src/ja/ganma/build.gradle | 12 ++ src/ja/ganma/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2107 bytes src/ja/ganma/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1252 bytes src/ja/ganma/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2472 bytes .../ganma/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4774 bytes .../ganma/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6001 bytes src/ja/ganma/res/web_hi_res_512.png | Bin 0 -> 4123 bytes .../tachiyomi/extension/ja/ganma/Ganma.kt | 126 +++++++++++ .../tachiyomi/extension/ja/ganma/GanmaApp.kt | 135 ++++++++++++ .../tachiyomi/extension/ja/ganma/GanmaDto.kt | 195 ++++++++++++++++++ .../extension/ja/ganma/GanmaFactory.kt | 48 +++++ 12 files changed, 518 insertions(+) create mode 100644 src/ja/ganma/AndroidManifest.xml create mode 100644 src/ja/ganma/build.gradle create mode 100644 src/ja/ganma/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/ja/ganma/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/ja/ganma/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/ja/ganma/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/ja/ganma/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/ja/ganma/res/web_hi_res_512.png create mode 100644 src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/Ganma.kt create mode 100644 src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaApp.kt create mode 100644 src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaDto.kt create mode 100644 src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaFactory.kt diff --git a/src/ja/ganma/AndroidManifest.xml b/src/ja/ganma/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/ja/ganma/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/ja/ganma/build.gradle b/src/ja/ganma/build.gradle new file mode 100644 index 000000000..5c0835975 --- /dev/null +++ b/src/ja/ganma/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'GANMA!' + pkgNameSuffix = 'ja.ganma' + extClass = '.GanmaFactory' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/ganma/res/mipmap-hdpi/ic_launcher.png b/src/ja/ganma/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cfde0e8ae83fa4dc67595456dff6022ba68b63a9 GIT binary patch literal 2107 zcmV-B2*me^P)RO#hBz~VvCjZPB zdpXP^AIKN-$%8d7yQs0T@rSPKHUl^VKp^aw(=ez|4YUCUF zm`@~8YAub&;|~iVrV14h`9?mHuekyuqU9446E7D?T4gdpK9aAwMiE_HIW{)-4-~kO zkK}7UfL4x z4X~aEz*=twYrYYzc_UcU2C(K$U@bR;b^a3k@GB6LdkY;95pf}tBM?I$gLT{qJGv5f zbOr3lxmxZEcBH}=>_|CS%S~`&Z$li@?8bE-oVg+=$LR6l_^P=S{P5rN zg|s+;#CUBcIC^1@rov%+H^bTYD4hQ7aQdHu)BA*8$O%~Wc>qKGnHbFUt03($xX0cFYx}9XCoLRbk#dM{Be^cz7Z#ATN8xne zmmzXA9IA2L!Pg;^L)kh5IH%yA_@|1rR7gIcQrLTzs!~AC^aT`ZuHT;5k4fg9ZqyRnP@sW!ki&YMI)I`I>ak!s)!n zizJF19X-D6`fDMl`+^1t5VUJWSEYy3awEj)&K&2V({P7Bh8ueaVzS++tqy^;-|nZD zFW7r7R^kJH3K}4Qwcn98t?ZiBaAR-gmR6Lmy?;>m5@c4t7ILyPCEdjEe_+?HP78>N zQpu--0tk>(`!m%!YGIZ3NN#1qbRSsbk5U$7zW?#mi`)cQ=Pyh+zH|W@6hM6NH9wt9 zfgjzG>(&dIIHtP$bGLMU;6?K?h?AdX0CW+YFCWb6?|#>S)IY&`?l(JgUyzA-UOSdM z^oc3}DT-M8&jEYXyh`m>to|AkjuLCW0dP()HXx{6XDzp+Z5>$qmh;`NoREnV0G~;_ zP*XHP@=SMwfBi?WhPA%H8m`f5yItSA*np_uQ|Vy#RgvYYWtjWkIQYItz?yFY@7u1% zBDosD6;ofCQUf^%d$9pQ&1G2AMx){M0QEmT_hNkVbJ(?4dLxWVIPG^pPIg?7QR@W; zw84aC$p5M#Ube43()PwDDdFg;C75zC}tEYdo~Hl-a` zIKA7>cem>NX}vHTQMEYN{{XDxE;wI31m5=~c>got1J8pGJnKvUtp90W;QiaR4*eC9 zjRj$mo>F5_bI?U%op)!sj>OcML-lZGpXK*j=?X$)=h7-sVh!uOv1|~w`R;quPXZ&$ z)yN{tR#&3`Tob?eDOOt-Bob9e*>pWi#4v#1W2W2Kng`_HTI=~@yDdq1n$sB0c%$2pRG0r>}B3K^u7FMK$#2zV&qfZ%=&Qr z=-IslcFh{dEN9dg3J}~A|MF(YWv0|3P7Uos87=CxyP0XF;Ot%ke&B^54$Ox9?K-WT zw%Z`t#C+MP`iANxJqs!^qeqWMruzdravgj_56#S)#*>0H-J9>o6>@eEPWS!k(~6{q zWzS{RVl2|oegsbEuhPQN^~^G`)|=rTe?O~E``p=&82+znepO9mrj;k^2~`gk>|>t+ zu)3=<9nW~@SeK|polYv|1m(`c9r>SLZ)o&k?p8+hP*oo2V;|qdw>7Y-&@;i919lQM zsohoJUq1EXEWJ;^*GKisFVS9MNT$2vyg*&CM$`(vX`pDT+3f8(A;?#Vf zJPXPLRQ3q?;G15Zv@BmZx(RLG1b*nHtUC3u0P^c3-Gs(JR1ZJ-k(6yp(esP`BY3Jm z3(G_?U!#K2J-P#|>(~0>&Bpa$jqB6{S9B)Enyy!;8MLd3$+obLTo(uyon9n|^oaHB zVKs=Csy_O@J_Z~VD*`G4Dgp{LpyS7nzbd6H)EDYNzLAgl0$S13)U=KBEFZK9o9TB} zr^!e1H6K9bTeoiA7>mWa3KbCfMn2~AN+!M3TWVR>nyRX*pB+4SuqTm7%y7;H=NuuI zAYaHQ@{N4-e9iS^1Bz&cWm#8k*|KHx&Ye4-jYgyI*45R05N44Nn+Q=KIB^O`Lf3IE#D6dq(oAw_sc516@OJowbJvU!t-gqpQbQBZlY+1 lT?_qzlcE)^Xhr+Z?Ee`s^9r)WedPcE002ovPDHLkV1mn33;+NC literal 0 HcmV?d00001 diff --git a/src/ja/ganma/res/mipmap-mdpi/ic_launcher.png b/src/ja/ganma/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..7c470f43ec01df2ad7f0d78d21d0bcd4cb7a39fd GIT binary patch literal 1252 zcmZwHX;70#7zgl|t6U;TP%$b}O#wTgv@ne=USJ}Jk{S*xR)nh5iWM(Z#DXc(v2v(I zqmT*|52VOc$Chvys6Y@92nq^vgd-3T3^4=)yYI1YU%&K2r#rv>KhHj2cV{nb6ZqR$ zv8^x+vk45~g)r6g{kAY+GP=*Q6vK=cZQB~gXO927#A5M(=5B6o|4sk^27{ryyIY}9 z&@@d^6ogQ#)iOOzBN|aOB56d>2&X9qpvf7Un5GFmO-vy|kC?=D2!D%kEy5=e)FAi< z!3hM%5qOQj7@8eMvnn*h7(z3H$S{E3^`PljXu39J*TD=l&+Q1wNTo6 zN-L)(Ybed%l%|@}R8ena)I=FIA*IIdlcT?r>LOBommIlGj@%+g3dmtbKB>whRk@_< z2C4d$9FmYj+2o*@9K1>nTqgS&nMD63QkhOFFA&N!qW2uplS1@}h*xKbSBXT|DWdBH z(HT#49w$1F5igGrFAm}DF?jnvynQeJJPLot*p0VE;))%(Vmsa%hPQ^`Eke9G7;oN+ zKMlg42EZnN*t8io@$tq@uwgxX!dM5Nd=Bf^z8IZr*eo_=o>Fum&o?ZgM=2f;?bzseo@I4)v3fN8pwMBbsYic;b3Zq> z>bJC64^>i_u;$k8a9v!fUu@P%))jBcIyY09bJ44})x6;-nUr{H^z_41*M`ycBR}Mw z`(^F=#jA^pO*T&yr&%bDUW{FEF?7;-ZPc#A{za=@ZmG6R>aaV!!DfwR%-JAp$LI!b zyn4?-(3#qn$S=)=S2+=a3`5%FIqZQ(`;&6hALB3c0(||CS^K!UcqD(n zkK?}AS9od6zEkc=8(!G=>6T`?r4@ht^#NlM0dG6Y^W9x|4G(S>_uO3|!5&S4Wn7li z;}c)K^q%Adi}t5DCr4x-D533Q&r4?|h5mgG1=ek|rV#hItub3uG+tSqvs}aC3pJeD zljGANqS3dNaW@Jq#`wa-L*ePtRxG$Bs$Ew*Y)7WueX zutMEiy6)asS>oE=hkIq+4nryK&G$=UcRCjqP1pu^oT&2*;kwl(M+h3Lqe{6)&Kgfw zev|ETs6FxmZ~5tr498u$%7a}t?3EpTyx#KbO`J6R;aInRRg6TqFj%)D_1Sl<0;9qK zLC&`ljry&z$S1>rzbJBJLfg_7|Iv0+k(1G~*RMDF#QimsB$riu)MYlo?~J{QLVR2_2hqIU2K1lY;ao^yXV8HL7@RGWQ zr&06S8lBBZ(jdrU`}hT0&MQwz>v=Li!@G04gI(Tmu38*=e1A|(#g%wbCh6z8;wRW` cYuF&p4`l~$vX+H^@%{%2vkPs++QMZM*DS@PI z-lR}UOI?afTYm_RDb&)gZApGeeoBtLw(KZcD{`!@Y_0WjwD!#0nS07Pa|g1+&d%)a ztm4_d2M+oo?TX+3%)RHHb9OId%9JTnrc9YKWy+K(Qx?o(%H6(P;2Eo9jMX#78X03v zjIk9p?oE0BSn>SyV&1QdPSyoaAVL*)=GC&r{1lb@X$tqOC7i=6H9K7AK ztZ`75*N_b|l21fkn9<*`Y15_~6#QT!AS?MK#KM4&Ygp+{TQ|B)hWq0(^v88Dpyy*y0SCNp|(+3ed(F z`;>xOtN?7aWEYA6YZTOC1z>BG07?KQfD%9n5Nd)4LZ1O~=>%AF`@qWV0Ba@=*6eN& zi|>HAG6f(^B|xaim&U-HcoU4{PlDFp1zLQi%iD5&$Ms+gKMXweTM|hLP>S(OV7~t* z==;9}TA~BAM7tY&e)#cLm$!hv`*z@`e`Y@&N&uf|H)es4{|2<3t8C~wzS8z01#n!8 zw*yZ;4Pt3r36RgsslS8Pe~T0Gxn5%QllMB_Yu_}Fs*maYV#sMlw}72!IEsK#}i}_2`3l13&RB z@OOU!eEdb*$DRW|`a{qUentLXu>#m`p4(F00toE_=(}(8iF|kTw}U>k0nDi_Ag-MA zdkYZ8JecRVfie6|`w0mMU+Zb2C&-!`s#XC2jNylhihOk4fviF=q@Ov5^2P64ZbqOH!3~2pp zgGGMd8n9;fgq@Ut$WDO1_j3iKpPWQ!0&D(2wFm&f-+SH*eD0dx*9Br>xKd{$+8^|N zpLehQInnR9e(?Shm%gh_0KN#?&Ko?U{-bMtJ6N-ou3iF&%V$CFTjP%Yydqe$gHaa% zz&!J+U*y|s{>1B{z@@TA|IMK9z0)hL1S^w}scOFHZ*Uxpkw>B~0DwMpk6*R}`u;DM z%btpB`kv3a(@wCax8;8@lTY~xKtUq(i%}H-sB@JQ@p(_U?O@FfmWvn(G#zcLnJC?(+H*;HQ4#Cjc179*e2~ zK>xgZt#`Yb-Wty4dL>QmNcUmOOug*&CzNq{!Pk1~ZBJYSAb8XS0E%j@Z?#8F$MKc4 z6oh5w^fo)=Liq6(Fh;%wU}g)Rma%17P=KC#5KHexO#obD&-+z0!`Xb@W?q&P66ruE zX@25+1>vJxnVnG+05{;Ip8&u|e^6vf`wAF`zYhG=PkkagsNpg+LigT5S?@Bcohyo}k6Fy`QDwU1i>ux1io;2o#gouFnM{f@h~4|@NtU`@Xr zN)}b60<;zEV)4}Pg1#K^)XQGY6>6~a@fYNVE|dWF612y!g-AuZpoXcSaqRKJ4VHK- zWi|MFxMW!u{tIGpgt8cMc@o6sbMmL-lT;-Y*)zG0zwbC^K;}}^1i;qgalhs{@X;R? zo7z(QF;H`*$E(uW;FtACud{u79SIpV0YGTeUf>-E{m}iTm3oc#} zVrMbs8V>VLZK2u@mZ3|HeD~5rp18K8k@u^k7>B)peDgZa&)ya=jyxE4<^qB*fVgy$ zPOxVBpH$H_Ctjz6+(+HOQn?$x>j^N9KN00}bkZI1?!hVACvZe@xlLfF4wtTc>%Jqz z_86FF{}l0t%;4RwIZgM#R5KWdzqZ``fsg*UVB{lw3J$&y@s?!jCBI==a;&*5ms#@% z?f=6o@)7>_m|abcdP@?s9NftALC^6-2d&$cHXMhiiR?m=PhJn!{Qjs5ARl+<@Z{xA zNnzmDk}8kIl&bXu_j={~xzT^<0sPX3>JR{`~7N52i$OfP_WC0L1KZV343_w8Dg zSB)p105lVNs5;8gUM3cfg0`d63%FM;$M!@g7{iay=YBE0HP8MT%n$xb(|3IAmtY)w z6trD8mh=8ZyB|==7UX zE){%nkw5W9wROaTHf@f|J)zMxpC1x@zsQfbfpz{r)!C8^YC^vN#^DFU2JaX7RJN|< z$vbLaRY0359oE8%dN26Bt@b!fg}($cnqStTUvxfAQzUr5$hT8Wo_vOy?lmzLQC_Wh zky1;2@Gj?R2nOFRfIL|7(?6#kbxn<>gk@M4LNTbliJx@zi}-jOouF|}D2&e?)Vcs} zIp{Pk7L!!p!P7COv)09an(?Gcs`|<@Wl8`gfD%9npaf6?C;_TP0Q~xT!!XnZ4gn2W zA+s!3}1}3Ymp*kx!kxE$;g6?(RD$ zCnqNr;KGKCkQFkM?8>@B#<@kQgE7{%apT4ZQmNEg&beakt{-v^$Oai9E6J=ejJw2~ zo0L$|Th^~%zhUdvt$#Rq^5po`)D%yr(@@Kf43GsfK{m)pvT6?NHocg<6|PlY&9s&= z_8G=lH)HGzjIj;1>Wr~_B!h0rWUXY>8rDsEZV_-(>6BMAbxF0?F~&Y!qrOfu=#or2 zC8LH)+^XtaYuF^;q+Q+?hi`gqje4hK&@P!YNk)~rU)Z_U*1pC$_S9@zuBZl4p0000@aK1&d**DHeA zOo`q^rKA8*zish0A>piiLB$-E|Bs`Fdy>T~oi+MLrDe76`dIJJpFb1iG!7XrOtN_u zj~TpsrN9IJN8DM#1a1*JJ3BAIZU!^`=ZTofd{-3s8QWai(sCx!{q#A^pgBOqkg~aX zB06a{p*Z3gedA?qY))-*aW7FzkhB$|gkPlN!NBfC0}O*#3F_DKYk4Nq`e*36?WyZ5+V%}DSCj3JuI?-yu zhmll?#pW=)DQrLy?uzGd6=?aV1G*^t^>=6XyGiTDO zaZu+R*TgdLkB<@KCI^=R345FhW<)rFJ{-5_K_(gP0);%kDMyT1h}90^l*Pl<|m;_JA--tK@2s8cBHIQEX+!b^q#ZTeC?DE$IAU{ zg^9%890B+~Na*!WU(b&R83_}N!EJ*7q*p6W-!jJ;;yBS@2*o@TE_06-?@pa@k_%cV zwoqq`^X9#z`kJX}jO4}#ec4e_VgyRr1RBEIK3TjU4;!kYFRM!TobCjcq~Qz$^ZK%b zCz_sk`-_51*#19eXgRu2fU6|58A@57qW#Ov7L!JzTqnFq3XV`crwVCIX)F$wJ z{L7&<&C*qs5T+242I^gA>l^B7TRnlY%f=9`5_7CgJzf1B9V@sX+42V_*4j^|SoTo~ zw_WAocdOmPf|i6=a}Ll=@wqDq9(YWn$+N))P;^ZbTiH7b6j34U!escrP1kQ}BPFqeii4+O zp|!1m`=f7ePO5ntCUg-Ig_k+Mz%o<(b5kO84)>-HN zaG)%9ucSO=UVF5GcBAhbi>X~_2I#}ef=heazm*r@q7u5j{Y~e+msDlSE%XC%m8yKV zA^l;I^+n@g>XSJK`Gw88-B|i~0pgF`^Y))O>dqO|ic6zH`&DJ>ThhuaX^(K~=Toy* zR)7)XT;V=$G-bPI3u<$L5C51l!w0-wXY!o9S^D z3u3C<9%@xN^(K9SQXapVs~UKzY27Q{T7SyFx>+YNjYhYRn{{?beRouVgSev7gG-Ia zaqZ9)f$y&9)v0BX_1#bCgx6=FRURX^B2D4 zzVE7(ca2=G6Pr*!S4bXVpV7~;e5_($n#|KY2plqsFA}4m!vy zrCxN~oipjU54d|>JtWB<3$8TWKF@Sp?sj;b01o+pDlDT2!fp?kwDi1)1$UPB*{Vp`M<(U?)|Sx+ zZn(FYBpE6~1&CBMNADcgW<)PCz{glgUnnNv@S#ISSp6q!?0fw0CX8u0dNMX^6E>5bXgD3ngl$rtp3z_$NX2nY!7V=31Ww!#jvj`r#&_MQ!DCl`8L0!K_Wq5L@*b{SU%xh=bms9+UxX? zv-{bN<@q;^b5il^K{Y|0qXVy~D|v;j%T#5JU&5}!Q=Z@M-eT_)+sHJoe0m($UQnR% z^K*dS0Pkdo;bq#OGe2ufsjUhHxLml>U=w>Jb#X~l><@EpK~O}!kOp*oYPKL=x8RpO zeW^;v25f6Py!4Yw+E|R|Asa_(IKQFR{IrW0W&BY&ZQzk}>B{43;h7PH%kvGom7|{U ztO%Pqz=5i1Jy^?A1HjV?6oq1U`n2|w)JAsVT}wW~AT6(isZ(Tthg>@>S4H6iwauex zj=HnPGu_{-FT6c9!FA-c150Y~U}%>dzKg~*L4FADYU2A6Z(DUODTr^ZMr{fu zTeD%0zFUMEKviZ;(~8sIz)KS`IIlN9!u_Y2yO1F__1*XXvY+omn*G#v#p7a#g^BeS9A`W+ep=RHjwBolBYOSpFW zy7ZugVJddRnwR2p`zP&JoMWh;9J~r=%i14yoi2xIS(-N}*T~0?e7W7`$p*Fo;|-+# zFUqL;ZBYi70DB>oaT8hi?_TL`@U@o2rAWqIxv}k$!!)77F>sS_a_;!=Rtr7* zzt1a`o1w;SZbg^? ziPmnde;voIJNntUoV;2T8$aRR3(wzM{jA$}4TiC#ZGXm5ojQ_+gS%G*ljq9(2Z`7|KXiS5oY&;acH#-zx*^1Y?$dpY!-Mi!LG?OmF(Gy-D zK{1W#%tJzW@-C~+vvqTM++_0kD!yg0N^Z3mTQ$cmJn9p#ymsp0*Usn2rr={iN!bedvNQQvlSwxi zaLWSOrLjJ4L_lFKZ81_UtD+|-Gew*mYlPccUM^0) z&T(g7E6hk0t{gQ-@@fj7uD#6tm8UR5St4DCQ25Sdu+k*o*Z8ASe>&rb#3I7_-jNh%B+s2 z1Pw>LX$!}Ap=+a?+ttocqU)Gcy2SU^`HnI|5}CA--4$fbRQ}~9os0fM_tH|2&0O3a zkArTb3-ltlv^9-Qox}IMpvowz!HDhq2rGX0FxQ(WRWkVGA z&fPML(0j3-E?#>tN106;h?b?mVI-8P6E-HRYG5_H$VB_~8`rn+!=rR<;Y4ftxgZMpM0i}k?+@&}g`H-+9x&8`oHqI-q+mJK z4q#Jc_-S{4%!ZEDh@1a>R4?-8(t(OmSN%*bQb?2~(cf#aADnFcU_J*|BXg^p4XNsa zQebu0>+KU--s48JVma4q7PpVoMpbYrQk#uU-^! zP$5FnhjYr_l^)MftOzgOSAWAxqo&JHJcv~zy1OyG5DQ!~tWLe@(36kanTRf^(`u&_JRS$DAPOQv!2n+Mh?;(scGMTZM pmQ8#wxEB=_`F|uft{;6yKaS~rad@5U%lZEkfY3MAE7x%j{~sVe9{K

=2nK`|(9%tu^aOVsB#gQku?4`%?v>!WXQvu@6P8 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 q&E@9cz3bu`KKA69XfaQHLBF@U;GCoSz0v! literal 0 HcmV?d00001 diff --git a/src/ja/ganma/res/web_hi_res_512.png b/src/ja/ganma/res/web_hi_res_512.png new file mode 100644 index 0000000000000000000000000000000000000000..416635d5b8e617d16f2e81dfb98396eee3e33510 GIT binary patch literal 4123 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 M>g?uJbq16CKU$`}w*UYD literal 0 HcmV?d00001 diff --git a/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/Ganma.kt b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/Ganma.kt new file mode 100644 index 000000000..ccb5bc40a --- /dev/null +++ b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/Ganma.kt @@ -0,0 +1,126 @@ +package eu.kanade.tachiyomi.extension.ja.ganma + +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservable +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 kotlinx.serialization.json.decodeFromStream +import okhttp3.Request +import okhttp3.Response +import rx.Observable + +open class Ganma : HttpSource(), ConfigurableSource { + override val id = sourceId + override val name = sourceName + override val lang = sourceLang + override val versionId = sourceVersionId + override val baseUrl = "https://ganma.jp" + override val supportsLatest = true + + override fun headersBuilder() = super.headersBuilder().add("X-From", baseUrl) + + override fun popularMangaRequest(page: Int) = + when (page) { + 1 -> GET("$baseUrl/api/1.0/ranking", headers) + else -> GET("$baseUrl/api/1.1/ranking?flag=Finish", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val list: List = response.parseAs() + return MangasPage(list.map { it.toSManga() }, false) + } + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/2.2/top", headers) + + override fun latestUpdatesParse(response: Response): MangasPage { + val list = response.parseAs().boxes.flatMap { it.panels } + .filter { it.newestStoryItem != null } + .sortedByDescending { it.newestStoryItem!!.release } + return MangasPage(list.map { it.toSManga() }, false) + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + val pageNumber = when (filters.size) { + 0 -> 1 + else -> (filters[0] as TypeFilter).state + 1 + } + return fetchPopularManga(pageNumber).map { mangasPage -> + MangasPage(mangasPage.mangas.filter { it.title.contains(query) }, false) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = + throw UnsupportedOperationException("Not used.") + + override fun searchMangaParse(response: Response): MangasPage = + throw UnsupportedOperationException("Not used.") + + // navigate Webview to web page + override fun mangaDetailsRequest(manga: SManga) = + GET("$baseUrl/${manga.url.alias()}", headers) + + protected open fun realMangaDetailsRequest(manga: SManga) = + GET("$baseUrl/api/1.0/magazines/web/${manga.url.alias()}", headers) + + override fun chapterListRequest(manga: SManga) = realMangaDetailsRequest(manga) + + override fun fetchMangaDetails(manga: SManga): Observable = + client.newCall(realMangaDetailsRequest(manga)).asObservableSuccess() + .map { mangaDetailsParse(it).apply { initialized = true } } + + override fun mangaDetailsParse(response: Response): SManga = + response.parseAs().toSMangaDetails() + + protected open fun List.sortedDescending() = this.asReversed() + + override fun chapterListParse(response: Response): List = + response.parseAs().getSChapterList().sortedDescending() + + override fun fetchPageList(chapter: SChapter): Observable> = + client.newCall(pageListRequest(chapter)).asObservable() + .map { pageListParse(chapter, it) } + + override fun pageListRequest(chapter: SChapter) = + GET("$baseUrl/api/1.0/magazines/web/${chapter.url.alias()}", headers) + + protected open fun pageListParse(chapter: SChapter, response: Response): List { + val manga: Magazine = response.parseAs() + val chapterId = chapter.url.substringAfter('/') + return manga.items.find { it.id == chapterId }!!.toPageList() + } + + final override fun pageListParse(response: Response): List = + throw UnsupportedOperationException("Not used.") + + override fun imageUrlParse(response: Response): String = + throw UnsupportedOperationException("Not used.") + + protected open class TypeFilter : Filter.Select("Type", arrayOf("Popular", "Completed")) + + override fun getFilterList() = FilterList(TypeFilter()) + + protected inline fun Response.parseAs(): T = use { + json.decodeFromStream>(it.body!!.byteStream()).root + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + EditTextPreference(screen.context).apply { + key = METADATA_PREF + title = "Metadata (Debug)" + setDefaultValue("") + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putString(METADATA_PREF, newValue as String).apply() + true + } + }.let { screen.addPreference(it) } + } +} diff --git a/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaApp.kt b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaApp.kt new file mode 100644 index 000000000..b11ca6dcb --- /dev/null +++ b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaApp.kt @@ -0,0 +1,135 @@ +package eu.kanade.tachiyomi.extension.ja.ganma + +import android.widget.Toast +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response + +class GanmaApp(private val metadata: Metadata) : Ganma() { + + override val client = network.client.newBuilder() + .cookieJar(Cookies(metadata.baseUrl.toHttpUrl().host, metadata.cookieName)) + .build() + + private val appHeaders: Headers = Headers.Builder().apply { + add("User-Agent", metadata.userAgent) + add("X-From", metadata.baseUrl) + }.build() + + override fun chapterListRequest(manga: SManga): Request { + checkSession() + return GET(metadata.baseUrl + String.format(metadata.magazineUrl, manga.url.mangaId()), appHeaders) + } + + override fun List.sortedDescending() = this + + override fun pageListRequest(chapter: SChapter): Request { + checkSession() + val (mangaId, chapterId) = chapter.url.chapterDir() + return GET(metadata.baseUrl + String.format(metadata.storyUrl, mangaId, chapterId), appHeaders) + } + + override fun pageListParse(chapter: SChapter, response: Response): List = + try { + response.parseAs().toPageList() + } catch (e: Exception) { + throw Exception("Chapter not available!") + } + + private fun checkSession() { + val expiration = preferences.getLong(SESSION_EXPIRATION_PREF, 0) + if (System.currentTimeMillis() + 60 * 1000 <= expiration) return // at least 1 minute + var field1 = preferences.getString(TOKEN_FIELD1_PREF, "")!! + var field2 = preferences.getString(TOKEN_FIELD2_PREF, "")!! + if (field1.isEmpty() || field2.isEmpty()) { + val response = client.newCall(POST(metadata.baseUrl + metadata.tokenUrl, appHeaders)).execute() + val token: JsonObject = response.parseAs() + field1 = token[metadata.tokenField1]!!.jsonPrimitive.content + field2 = token[metadata.tokenField2]!!.jsonPrimitive.content + } + val requestBody = FormBody.Builder().apply { + add(metadata.tokenField1, field1) + add(metadata.tokenField2, field2) + }.build() + val response = client.newCall(POST(metadata.baseUrl + metadata.sessionUrl, appHeaders, requestBody)).execute() + val session: Session = response.parseAs() + preferences.edit().apply { + putString(TOKEN_FIELD1_PREF, field1) + putString(TOKEN_FIELD2_PREF, field2) + putLong(SESSION_EXPIRATION_PREF, session.expire) + }.apply() + } + + private fun clearSession(clearToken: Boolean) { + preferences.edit().apply { + putString(SESSION_PREF, "") + putLong(SESSION_EXPIRATION_PREF, 0) + if (clearToken) { + putString(TOKEN_FIELD1_PREF, "") + putString(TOKEN_FIELD2_PREF, "") + } + }.apply() + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + super.setupPreferenceScreen(screen) + SwitchPreferenceCompat(screen.context).apply { + title = "Clear session" + setOnPreferenceClickListener { + clearSession(clearToken = false) + Toast.makeText(screen.context, "Session cleared", Toast.LENGTH_SHORT).show() + false + } + }.let { screen.addPreference(it) } + SwitchPreferenceCompat(screen.context).apply { + title = "Clear token" + setOnPreferenceClickListener { + clearSession(clearToken = true) + Toast.makeText(screen.context, "Token cleared", Toast.LENGTH_SHORT).show() + false + } + }.let { screen.addPreference(it) } + } + + class Cookies(private val host: String, private val name: String) : CookieJar { + override fun loadForRequest(url: HttpUrl): List { + if (url.host != host) return emptyList() + val cookie = Cookie.Builder().apply { + name(name) + value(preferences.getString(SESSION_PREF, "")!!) + domain(host) + }.build() + return listOf(cookie) + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + if (url.host != host) return + for (cookie in cookies) { + if (cookie.name == name) { + preferences.edit().putString(SESSION_PREF, cookie.value).apply() + } + } + } + } + + companion object { + private const val TOKEN_FIELD1_PREF = "TOKEN_FIELD1" + private const val TOKEN_FIELD2_PREF = "TOKEN_FIELD2" + private const val SESSION_PREF = "SESSION" + private const val SESSION_EXPIRATION_PREF = "SESSION_EXPIRATION" + } +} diff --git a/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaDto.kt b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaDto.kt new file mode 100644 index 000000000..31aa51073 --- /dev/null +++ b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaDto.kt @@ -0,0 +1,195 @@ +package eu.kanade.tachiyomi.extension.ja.ganma + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable + +@Serializable +data class Result(val root: T) + +// Manga +@Serializable +data class Magazine( + val id: String, + val alias: String? = null, + val title: String, + val description: String? = null, + val squareImage: File? = null, +// val squareWithLogoImage: File? = null, + val author: Author? = null, + val newestStoryItem: Story? = null, + val flags: Flags? = null, + val announcement: Announcement? = null, + val items: List = emptyList(), +) { + fun toSManga() = SManga.create().apply { + url = "${alias!!}#$id" + title = this@Magazine.title + thumbnail_url = squareImage!!.url + } + + fun toSMangaDetails() = toSManga().apply { + author = this@Magazine.author?.penName + val flagsText = flags?.toText() + description = generateDescription(flagsText) + status = when { + flags?.isFinish == true -> SManga.COMPLETED + !flagsText.isNullOrEmpty() -> SManga.ONGOING + else -> SManga.UNKNOWN + } + } + + private fun generateDescription(flagsText: String?): String { + val result = mutableListOf() + if (!flagsText.isNullOrEmpty()) result.add("Updates: $flagsText") + if (announcement != null) result.add("Announcement: ${announcement.text}") + if (description != null) result.add(description) + return result.joinToString("\n\n") + } + + fun getSChapterList() = items.map { + SChapter.create().apply { + url = "${alias!!}#$id/${it.id ?: it.storyId}" + val prefix = if (it.kind == "free") "" else "🔒 " + name = if (it.subtitle != null) "$prefix${it.title} ${it.subtitle}" else "$prefix${it.title}" + date_upload = it.releaseStart ?: -1 + } + } +} + +fun String.alias() = this.substringBefore('#') +fun String.mangaId() = this.substringAfter('#') +fun String.chapterDir(): Pair = + with(this.substringAfter('#')) { + // this == [mangaId-UUID]/[chapterId-UUID] + Pair(substring(0, 36), substring(37, 37 + 36)) + } + +// Chapter +@Serializable +data class Story( + val id: String? = null, + val storyId: String? = null, + val title: String, + val subtitle: String? = null, + val release: Long = 0, + val releaseStart: Long? = null, + val page: Directory? = null, + val afterwordImage: File? = null, + val kind: String? = null, +) { + fun toPageList(): List { + val result = page!!.toPageList() + if (afterwordImage != null) { + result.add(Page(result.size, imageUrl = afterwordImage.url)) + } + return result + } +} + +@Serializable +data class File(val url: String) + +@Serializable +data class Author(val penName: String? = null) + +@Serializable +data class Top(val boxes: List) + +@Serializable +data class Box(val panels: List) + +@Serializable +data class Flags( + val isMonday: Boolean = false, + val isTuesday: Boolean = false, + val isWednesday: Boolean = false, + val isThursday: Boolean = false, + val isFriday: Boolean = false, + val isSaturday: Boolean = false, + val isSunday: Boolean = false, + + val isWeekly: Boolean = false, + val isEveryOtherWeek: Boolean = false, + val isThreeConsecutiveWeeks: Boolean = false, + val isMonthly: Boolean = false, + + val isFinish: Boolean = false, +// val isMGAward: Boolean = false, +// val isNew: Boolean = false, +) { + fun toText(): String { + val result = mutableListOf() + val days = mutableListOf() + arrayOf(isWeekly, isEveryOtherWeek, isThreeConsecutiveWeeks, isMonthly) + .forEachIndexed { i, value -> if (value) result.add(weekText[i]) } + arrayOf(isMonday, isTuesday, isWednesday, isThursday, isFriday, isSaturday, isSunday) + .forEachIndexed { i, value -> if (value) days.add(dayText[i] + "s") } + if (days.size == 7) { + result.add("every day") + } else if (days.size != 0) { + days[0] = "on " + days[0] + result += days + } + return result.joinToString(", ") + } + + companion object { + private val weekText = arrayOf("every week", "every other week", "three weeks in a row", "every month") + private val dayText = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") + } +} + +@Serializable +data class Announcement(val text: String) + +@Serializable +data class Directory( + val baseUrl: String, + val token: String, + val files: List, +) { + fun toPageList(): MutableList = + files.mapIndexedTo(ArrayList(files.size + 1)) { i, file -> + Page(i, imageUrl = "$baseUrl$file?$token") + } +} + +@Serializable +data class AppStory(val pages: List) { + fun toPageList(): List { + val result = ArrayList(pages.size) + pages.forEach { + if (it.imageURL != null) + result.add(Page(result.size, imageUrl = it.imageURL.url)) + else if (it.afterwordImageURL != null) + result.add(Page(result.size, imageUrl = it.afterwordImageURL.url)) + } + return result + } +} + +@Serializable +data class AppPage( + val imageURL: File? = null, + val afterwordImageURL: File? = null, +) + +// Please keep the data private to support the site, +// otherwise they might change their APIs. +@Serializable +data class Metadata( + val userAgent: String, + val baseUrl: String, + val tokenUrl: String, + val tokenField1: String, + val tokenField2: String, + val sessionUrl: String, + val cookieName: String, + val magazineUrl: String, + val storyUrl: String, +) + +@Serializable +data class Session(val expire: Long) diff --git a/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaFactory.kt b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaFactory.kt new file mode 100644 index 000000000..862c4afbe --- /dev/null +++ b/src/ja/ganma/src/eu/kanade/tachiyomi/extension/ja/ganma/GanmaFactory.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.extension.ja.ganma + +import android.app.Application +import android.content.SharedPreferences +import android.util.Base64 +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.security.MessageDigest + +// source ID needed before class construction +// generated by running main() below +const val sourceId = 8045942616403978870 +const val sourceName = "GANMA!" +const val sourceLang = "ja" +const val sourceVersionId = 1 // != extension version code +const val METADATA_PREF = "METADATA" + +val json: Json = Injekt.get() +val preferences: SharedPreferences = + Injekt.get().getSharedPreferences("source_$sourceId", 0x0000) + +class GanmaFactory : SourceFactory { + override fun createSources(): List { + val source = try { + val metadata = preferences.getString(METADATA_PREF, "")!! + .also { if (it.isEmpty()) throw Exception() } + .let { Base64.decode(it.toByteArray(), Base64.DEFAULT) } + GanmaApp(json.decodeFromString(String(metadata))) + } catch (e: Exception) { + Ganma() + } + return listOf(source) + } +} + +fun main() { + println(getSourceId()) // unfortunately there's no constexpr in Kotlin +} + +fun getSourceId() = run { // copied from HttpSource + val key = "${sourceName.lowercase()}/$sourceLang/$sourceVersionId" + val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) + (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE +}