From 23f84c24165c03be5cd2473b17e271ef9f942cd2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 12 Jul 2021 20:43:40 +0300 Subject: [PATCH 001/180] Add Weblate badge to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce1897442..fddaf1d25 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Kotatsu is a free and open source manga reader for Android. -![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) +![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/nv95/Kotatsu) [![Build Status](https://travis-ci.org/nv95/Kotatsu.svg?branch=master)](https://travis-ci.org/nv95/Kotatsu) ![License](https://img.shields.io/github/license/nv95/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![4pda](https://img.shields.io/badge/discuss-4pda-2982CC)](http://4pda.ru/forum/index.php?showtopic=697669) ### Download From 7c6a97e264c772872359c782ee429c7e93a1c03f Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Sat, 10 Jul 2021 22:37:44 +0300 Subject: [PATCH 002/180] Using Totoro vector icon --- app/src/main/res/drawable-hdpi/ic_totoro.webp | Bin 1654 -> 0 bytes app/src/main/res/drawable-mdpi/ic_totoro.webp | Bin 1250 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_totoro.webp | Bin 2524 -> 0 bytes app/src/main/res/drawable-xxhdpi/ic_totoro.webp | Bin 3120 -> 0 bytes app/src/main/res/drawable-xxxhdpi/ic_totoro.webp | Bin 5222 -> 0 bytes app/src/main/res/drawable/ic_totoro.xml | 14 ++++++++++++++ 6 files changed, 14 insertions(+) delete mode 100644 app/src/main/res/drawable-hdpi/ic_totoro.webp delete mode 100644 app/src/main/res/drawable-mdpi/ic_totoro.webp delete mode 100644 app/src/main/res/drawable-xhdpi/ic_totoro.webp delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_totoro.webp delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_totoro.webp create mode 100644 app/src/main/res/drawable/ic_totoro.xml diff --git a/app/src/main/res/drawable-hdpi/ic_totoro.webp b/app/src/main/res/drawable-hdpi/ic_totoro.webp deleted file mode 100644 index 0430efcfc8fe16ad55cbeaaef597fb47776f2629..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1654 zcmV-+28sDnNk&F)1^@t8MM6+kP&iCt1^@srN5ByfFXkYUWLN3^WDBUpBr*^X-~Ip| z;5L%vNQyfCufP8up8jmoaU@BOlD&6`KmNOg6TGx$+l^$qZDT+-6G-P=y7&FB&B$RGAb-a?}V z|2#BOM8x6>MF>J|K+;D7;Q$)J5aSR_4XqFqs;;DuBBYXyD}a(B96%WVK%E5?y}3ce zQ6Pa41BlTAE`SH2f&t z=fY|5AlEdd^x8J{+3Og`@k$G;GHxDxV{g^`J-hSYK3AmQ|JUWi;BhdY4}1J<$(w(^ z&&|~pcPA()TH9twlA>yC+qONk-W}U*yL+m#8e`iwTVva-svGN!_%kB+2J-tOqW=)w zwvi;+DIM2z@`R=2u=MoL2n|{`*zT;kuSI2?q|_SMc3?9A>TwQy^?zl0ER- z{i7}|FQG`R1QHG4vv*0f9GR?wOpeGTyM)iKWyS4VNwz9StT zW**Z{ql8yA_v1lk<~wnL15_$|0jp(%LtkfCF*8RSwO_}wq>*MzloteWx6?XR{|TV^ zgQibZu(GTx!oZ=Cc~J%Uq6BhOm6b3qDiqQD$;Rj4(+hd_d4o&wvD`@+H=2>+lm{-y z2AQes#V29z)&hAj06ToL^kQ2VV2tBoS>ES!oD0+T zV(H1Sx0=;Cyy;xGZ{0mfc~#T1gH_%%-M@AFXA^Us^;YO6GjpsYmA$<-$!j%Ws)LyO zq{CW|@4Ls*bt$xgw)7QxPQLuN*74DqM>;2jzQ1Q{aK`F#5LgYIY{N1!)B9&PUjA@- z_~l{mAI&t=1`osw-Nw11A=UM4HUFUMpc`Y%_r=PVs|;NIvzff|(DJ zBJs4ynIbSlCaDPN=(&s(dk-v{5SLIsjixhNE;{iE*MaxS`ajpvahWI$NM^X0ok9X1 zXjvfC$sJgFQb76ggpQOuai73z{V1~IMa7iY}{pqbnr42X+{B14^9FqxxMsKyQ`+pR)_SUZk znaH;`qV@?;|SLB=5;l=sP7i7c7O-T(Qf38?*V6W&rs=AOsvrn(Na zQO@H^jS#JXTOUkC)d&7VN9|-Hxn7xttXKR-N9t5d_0vqV1yMf(tzeKz{(z=t4oHfA1sCgg{J3sKWV z&%-Cj2kxhF^G36!Id%>R{1Sl1_5){~+rGQHup(0PP=IZuqOf}R_H(lac4*Wk1#=HH AApigX diff --git a/app/src/main/res/drawable-mdpi/ic_totoro.webp b/app/src/main/res/drawable-mdpi/ic_totoro.webp deleted file mode 100644 index bfc0242b21c4b449ff341df27b22a3a95e19f897..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1250 zcmV<81ReWQNk&H61ONb6MM6+kP&iD@1ONapFTe{BKjkWtBw5LR?-vEA#UwHia8AEV zgri2194V^j{QvkfTmZHlN0Q_y*?Wh6;=fxs!Aomsx3%rIjlpOUrcF??n2~j>gY?qrkj@ui@WN~%HHpN4 z8xgDh0TI9|P@n>+Lx4vh1_mAu0fbakAk?9P6mmmQi1-L8e@JL4AcZ2N$|*;Gfglxv zNCA#&;@TumLlHwnBF!IKcBf3Nz_|tfMyy0A3f3$Z3Idv8kdcc5JD^3QBVL*o8!?3i zfnYCe2h5@G55Vk8u*U~$ZFypi+KuE);Y)MP%$+12uG{sT`9e?2iUIVPOO^#%Df|C(L?x%#n770Uobo{Sh{5aAy7#8`{v;A;B~zz7z~C^qKcLnby7{V?ix?y9~C zhi1DOP++CGkbk908(U+vH@S*0k&5J!n%=^4zn?y$PcLo&oEN=eUd@8KP6xJZ` zI78Di_xV|o0q4Q~=*Q#Dtfo*8B`v$jcY)Gn4vny;U$zN@M9Z4Em+v>1>Aw(BAekSu zIka*#z4GCRc}VG_p+ka|WLG=3|7Yrhq}eTsv#7iuFdHfrHZIO4JxKZ4uOf6LQgdQY z3sWfbKCH?`%Xz9XM?%WC;mcXfvrwtYDouD6S#L=9=ghS#l`kqTAX^nt7@_Mkj$RvO zQys^)UVR*IEG``HkiBg5Q4_h6rGL*>Nv*w@WKtwExf6Qs*9(Sfca8-@uMgyOAp6Us zP+)9ltzf7AzP4s}2&1dtO$BeIiI!pLlAU^24FKMbSsii0j_ckot~%7n6f)md(Ocf^ zNLj7@F6{TqTyVdzUZkm1oV`CdcTjjf>vix8gvuB5yM?98K0jHv-3V*%o_`@USho(L zi!Qj=g*D8Y35O=uh#s!H7639eHN>}55*R<`l}EQzAOCVMUYaOn);l&HDB&9%Qgd5) zM$XfJpVvS5+{)UT0Bb;>*JJy?r;hL}H>ugjfQBp1G z3@9I0v8J5?*)J9gnJ*c7k<9>`pkiqo{sV1R#aa&k;tvye)yi3`Vnrpuhn(@pX>mxU zQL&KEz!!S&`6)}{LV1fS=nDYeC#8NMsZS0lO9%m>5B(tZp)Lw0`BFlk>&skow*o-r zq#D`*K)Od7Deln@P0=$U#STXAsiqY7wCb6T;iCZjMrzE~Bp*d|r&92w%3(St<#5Fv zUq!)vOp=uIiX%N2^al;3ckI&Ly^Y9 MzlT5ETLO(W04@ewtN;K2 diff --git a/app/src/main/res/drawable-xhdpi/ic_totoro.webp b/app/src/main/res/drawable-xhdpi/ic_totoro.webp deleted file mode 100644 index 2b0dde6ca33b3eff8f375eac39984bc4fe8849a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2524 zcmV<22_yDWNk&H02><|BMM6+kP&iD-2><{uU%(d-7Y8?zB-u*#|DQw0VXQ?(yJ0$J z!3f8VBq>r~ec12jxSY$QpMbk)n-hgO{3=N11O zz*IB0RIRXW+a`M_?HO26JOlS@|J7{FUz2)q85P&q5a7Yf_`yg5v54iwA;kX!{ub;c z=uA3s2;;eA;}8<2^$;cCutbR*0E8fQDlnFTG7`iw)x$|HzyS^q0Yg_nT6i_6fdQ&QjjHm7S1L6I zfE7q3>dS`=R_2Qa;r@t>0u$6_lhur%8O`#QZl?9inn4%S zjn_l#W)5A(`rnaM0{}4C#>+4MQb0li4QLJ1z5z9yb#8q{L*~@AjaQRoUZVWnsoMCD zBo4XTkFKA8IrHJe9NWj=XLx_A-f{^S!_#kv^FtnYy*T03O$rO!wkb}t&898{tp;BB z>hA7B-QC^Y-Q8W0x)-RC7Lsh;-TS42BtM0neRgK{59~7&(SHYSBPmkV^)Y0q2kr-i z-@&x%@>7U~C-kYBObZhG{bz6`I~*zWmnrIhl~S!7q{YZYSTQbkl5@`Q8d4N_YKDRCQUd|pNPHIUg>6!%!6>`NB~D^#8xknfUC}f zaAZ}8N1?4gE+O|Xon^9eK%9NVR&N0d5S5r@a-sxrwh}v?|6Uhhm5(;s-CTtFe?=Sp zrJo3?FKDB$HV`3nh>gDVNQ4|DHoB8vgd7xH6wUqPYY=IKg8_;h;~rOHC{8R^G!X<2 znlOW3f4GrfN18Pl(C$QO7eUPO(BLoETscwEGMcVg{LyD(@MIB0SLs>p1~h24k~{uA zG)NyL4!=i&ipH*>$2Gof(ZAvepy7YZ-6hpI#%rOv08p9 z7t>5qmrm_tf7KjlQZ-sifWwF4tEc{-Zn-HslTEt7#x#XO((a~Ki$4@*RmL-2=C;_I zjfg6qjlI)t15b5gV@jZK_xAAW@>vF}vR_CkWlYW8=kvO;q08Li2HuEVMyS^L=i1ev z#!0ViR%!bloAPKCo!M=_qr=Wrnf@mKwTAU&t*-g-AZZ7FaRAQ!tMdqwK1VM z<3Q4G&$O=L0d69|XWv?&<_yZDa0+iPn%L%PWuJpm&0Kt&-p+g(VD+K5XGLhc1*vxr zmFx1)k9igY(Kx-voTHYzxA%B0Ft-Z`R}#z_OYclGhI#@P{1U5nrm|ZwEf&T}a`7t- z#;h{OK>3xDqcb^orejbYvuMY;+)>yUg!HX7DnFuAv}Cku6G0I3PdXq=<3e6tgjb?7 z@(hyK9E(%u$wfrWn*pO-rgcvo9DrkM3%k`*Yv;j*#PUF+B!4P5Hn~0e4&f?b? z=7o0-K~!}ike0vrw5TRTxHgP<=Y>Uh)_uP>-eSrQytqR!oC1N=T*Rl9&@g&h^rnK% zU=e(YJE_&sw`mIQNn;8K<~;>4yes)M3gC;qKoCDa0;6LoQAtjlNNQ zg33SCC`5PgxoTx0ys~c~oEZ ztAHE7nqgA()#iqIGQOE?t>o(_r114N#zN5zO`_M>^$|ep zRrKjgum3?v7p&z#O#hx-BB=EOkkZ+;9zfef5cl6NQaECrlNn-FD0}?6lHnp%+)ofU z0Z8lFwF^L-kBDPrn+2CP*7fUkHkMt$@LdySeUp>uaUL$EdlLD;}Q3FzDi(jnz_R>sN z*}&+d3v?eu#L8xhm)}@b5}1=88yMB3K%>D4Y$;R(tCpIxs_kMwiCR+61ZMjKM{&!p zx0t&&hrUCnaP$`mwAB~A`p&9sD7DbzTA;1QwUfcBpU{e(%1eG-m>sLUG~g$z3ZT3} z<<)p8%+5lB+6w?K5Jasrz!9Z* zq(PY@whTc$m#7P{I){kmpxXu-j}Y8;Q0rI1r1mw--f>>=osogBgQwO5d5><^K!kp3U$&?|pOD&@4Fmi48qUB!Z$Rov zdB%lrU%3bo^Cp2PZ~pKKSAYirrhRc+_I+_DYG1q-Tr+tE{7o4KUpDRQ|E^^`NXto~ z)<1LLL&$+)e&KxhKa!Z0R6Q?aIhRmFso5*IG6tlnG*3X_UyEZ~?`Zm5Wr4IfS9w0> zJlnZSr@tbEH!aqnIfK4XG=;aX%xu%ZI2UTrNnT_<7xLXCV!YO(F1XIMCR+6e?^+_r z;pBi7@4J~S=W_C1N>i)y2a>}?l*_!YJ#E69zjlW;?!fUF;zCf0<$fb>VKMP^@p%6CKVC!GYSRN@Bx7muO*lc?FSC;Y=89 z@R2?h8GbP~NjcUmQ5E|fL0jzlvU*0>W^dr!5TBYQQ{m-s&YPxPCa?bDB6e9ux>c!F mr$%;fQahnSuD`}%7eRSgIk^=QYQOA0^3iTsIo zaMfDmiXPjlz>MMvypQ9zS(tw`m9bv|EHi|0trU`Q0Q;0=fI@;8254YD5+b4M0DwRg zSQQvXfkUbS(!s9paK8@1OQY3001BW0H^=}009650f25%U>F5P!GwPpM$s^&!2t{qO^6I*sTLrF z1r~|KXuV;HC5)w{i#Axac@SUpQ86SuAOePIKmn?OLyqN0LO7bz2o?@bSO+TyLj%}{ zAlr!a0Yl6iK;4yuVHi%wnGUCmkN39A+}|pV1FdEb!V-6Vr3tEhCc>3?w4QU6~x zYxE~ew$(15IDYjaz3y${=29+v&YdA7Tzczv@EW%r z;Kl&59daAkX4;t#%k#<33_Q=6#vSJUChOOL98NR5t~Ym_e^^dH+;MM@tITjl<11tY znZrMHzv0JKuZY63|E1{yf&hqyHj{JF3hs0Z)Xx89;m3HOX++pm^ zBkXdfJipT=R+i<6u-vY4-{Z50=#7MWZzob-Q*4PPEo64CEh|DKWu%X=H zn<)HFRu?8*51<#=@2{EMse0JSbV5XLlEcUm(a{^<{+ub}H__3)>xO1Ip*P97*@-Ei zAk&%SB6M0Rqg-Syo96^*c1);;c=O`vyI(JG>#7Xj31-NM4ZZvH#apOvwdSd?P>GY{ zGJbVZ5`m?*sD|1nzAA!HC&NJ(LH2B6AV3F3XMz@em0+HjJRB0MVRyce6Gb*NTR%cX zh1KkW803S4=#@huRP?{-{A^A{TbRQwL`1WyiT)1=IuyO~&IpL2U4AXUdb~>^x1M!! z)++_(a`i{;A*~M*dsj-|BQFLdS-EQvoHYb5w z^tYW~Kgx+{xfic<#t3gwbQq$T*2ZSmcGTLU}}>$rg5h$)VTU($bqtvZD%p?N%_#1 zSJgl~IyJ1Cp1}-c>Q??c&HJVuC}&DneEmy-m*v0vbtS4_ZR007-jwq}P#3kG;6=!E zz7p~xEYZk#h1`#f8}!ZY6NgS#^8-9V-wYTRagRPV^tl*w$(%T-@1G1@(!b3_)1}x& zeOi&LX+Ixl($55Gu25@jN=!F$o5h){@fN-t<|SRWarv=_uQAszuP=~CTXb0@&3yYT zQ}XN^ByEu<|B&pI>jraZoa+DMm!n_I4WUn?-}xcv=NC2GRe-lr&V2Oi9fW%>KXWF)r0lIc|rE=8`hCXi8G^8YY-NgHC zFldx7g}&DyFY!~g#0qq&TxqX5=_`2kCY93 z0VW1XpjxUuDq$3gw}-gG*hhdkQbISy;_0xWE23i|t~4GHoy&lw#nt+U;9F7nXD(OV zuP(psU0sgpNXF+}tqnk0pS zN(+{npqG_890s*UVGojMF8eOdY~$b0HziOYz?Pu!VU->xXnLjKkBEd=PlGu&dioL~ zX9Ynzgx09E0MrtN&yvI+8F-@vdQX6@Knqk#fh;K(+#Qlp0=pt3^tGUTDP*QXB@sw_ zW{uA$7Y;V2WEW^k+=5*okrH(tCSbU>pr$Ehqb0d-Hm9IrvgHRU;XQ)d0f5%1Gas@5 ztx0o8YHX9idgX9oPH!}N`ci0Ck_A72OjhQ|Jjgb|UnE@8`z-dE>3}FJ${j|LJ59a! zEZ#p;K!IQzWZn@~PKF?d1Z^$LHkUC(JowP(4x~@sbPy4-Z0%%AS3xNxc2br71e}D{ zvK&s!HZc2m(LAaHX;p1&D^O(N7!@GxYR zDz5;-bp(TGodLmZ@&be!5AuP6ePO^TMJfvb`BwxlM-2u(D-S_vL|+zM$p`oqYMdot zw8~OeV0UW^CX4=X?cf%?X_tT1QWXWzSvB@UhNJasq6ZCzn0(J1PSNd0EZgKYwj8-9 z%4yCw$qdnG!s(a=M?j9Nu>;aV(3(@^K?5JiF*y;LnY|45y)VyiibjxWaw0NwdKGvG*!PqR1ENSC=gB5FA&85AvNX!=$hbT{YDE)2sl@bbjZJg zFZ3HGI0i{mV-lo=ptXLh1l=K@t1$vHTyQ|YdV&#G- z`ByCXvTmO->$!Z$YBlbKgzE?f>efcEFATX?jlUtZLC{Y(6g*7OztuPonj|R8(W}@O zECy9m!$^ftsRi5W)fKH52%2K3fscc>3I5V65Hto2V2YjrEf6Hl)@fEC_yAN(mBR&) z@M#O)73dTE#jJi9QgB$6_$&x@5yYqHgK7a3_a0?K%6ySGy1Soujz|B;;oC7jT z1(mPr@M<9NO;GV=mGDatJr}oN$sG02XV&--0>8tQJ_e&L0{k;qHUAt6w-UUbT8Xi+ zjB^647gH;Aj0ykDfQ?D0g$-adN`yN?DxuHBqX>Qj6+e;zqrnLQ_AgW??*#7CPh|+&KF7FJb zarp~;z3_C7JR)=Apc{X}O8T~$XmS^eYct)yal;xJH{ibACrVFN^Mg{P`v#1QaF@3; z{hxP9`RCCCE0%c)23~+CQ@vx!QRa>P3cUjRGp}4{dV~2OdZzJ{H?&6Yz=~hI;FkbB zb(nr%7|dG}9hSP2KHDdEFi0@(tub(EUPRXT3WmXWS$^J}_BEJCCocW5Yx$@`37=Fj zL=2viQ1EisT{SKxn8(NK*6iP_`JkaszC2|~R$&2UEH7h1Kggtl!mK4zUVie0LCt&h Kt64YBGz0*Se!AiS diff --git a/app/src/main/res/drawable-xxxhdpi/ic_totoro.webp b/app/src/main/res/drawable-xxxhdpi/ic_totoro.webp deleted file mode 100644 index 4193ce397dbdc2b2cc3e3cc252c02694bbd02f7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5222 zcmV-s6q)N%Nk&Fq6aWBMMM6+kP&iCc6aWA(zrZgL@8>p>BuU9$r1KhUQPIEcm<1!? zMv`PJ%3l8H`0o{FoO&ANIFcks$=*YY7yi4&69X_n!!}&owpBHpgR^n3A@cuqej_z~ zD&O~JJlR`e+cwCU;SCIE+zFrS;#F48UwcJ?O%*7)F?ett-)M;8LG&jM!T$&R7R)5* zOgeE0&AH?+jt(t^0;517KqFMB#2ies6lu=YF%q;XL%a%t0g_+|0m)!#K&nlEI0IB6 zfMAOttTsLiib+Zalf++$PJ^Z@DrFj7m|bvLVuCRy0DvXNQ#X<<5eOu5;-*Du0>AhL zq4_3MYJklB7zLyiCN+4T7-@tZfCLtrI96CS9Tr$v9^u2PVpKv<8mp+VECz^_RcHct zZjLt)#Ak4zItt9trWzCo3Y2W)zsK16YiMB_xzx&Mj!7Dyy?nL%&0Fx1!n)=+k(N|( z)5}s5>c6u2i1Kx&_xZ5b{q%c1Rez?}`MdkWCR}q}|F8G!rFFl^`yt=E9?tP}lffJR z|EiVv|F)q7s3MgJ8v`+PID zENjz?+}^Z)t?E6qKleoc4U~Tnh~2QCN>hLY3Z3V@q! zz>>dKAk%Uw4ty%Cmaza@!W}1FLZ;>45G)0u3YnJvSQ73d)A9wDgg23CS%xKHDKagq zup~T=OiO1h3Db~iX@eyp7MYe9ED0@;X_2Jbv8+&JR%$Y_BxEXNPHLaS((t!QGnm&S zu{1;~WJZeC=vW$bRfJ5)E9qDwY_A}hDQ`09YBPY%wJ5{b_0Y-9IXx3XCN~GTsB3`0 znq3{#M2-UGp?G_YBd6O(4Ls;J&hCbh=Z#7z^v%QUWON+%Z{a`F06sIE^r$=1(du>1U}mQDsIK?B4=C}2UZsR|`Pj4B6i@MQ zHQnjK&3t{ZyH0UOgQz3>x|h6VHq3})q`UT2NGGtPm%Gy7*JlP&b-D}xWfQwWbbgyY ztE|`g(C7s1AuEnB%!c2)gr8S3z$yLxdZEhCHu)Lo*h`RLl+n$y^ma*==%G{tmpqNs zY=PN`yGZDH{|u4xyM`+JwKFbx9KvrkGXMdm{Wh|zfrvheqWQ;~AL0Tq97;~S^11JS z*qCZ)wRYqJNM>iI_=8*v2gITWryF>LCQvNzlwG`Zd^QHtnAFpnRbISwbC!`+dN8Ee z4m?BLXup3#|){Onu;j)kc2*ur+)#0wb~=Gi7+UteTR7m&W`RAIuQqWW!q( zT+nIccXcvc(8}ue*Ql2YuFBLPc3p0~G#ngcuwmk;G5l;8RwDnxABwQ0jW0@>%pANc z8`{?DW8~KZd^#*;?;zVGr9ZP8mt* zxw+1|HHh_&UUb{yIeNX#=3A7v+B2{=LTVzCovMx)hW$OFO7~Xo?fHPc8eLc0>t35~ zevkO&?RV_J06JD7$$1@I0r|!C^mST|d$cE>7Mlke`Q9YDDXF~OkH(_2tZrp??8xEvbpI;T6Wfcf0#xJzhv1LV$H>gu;d!+G-9`H@? z0B*u*${-1HZ+v~@KyNFcFnrv-5j54_lRAuiX%e^Sjql>sQRipej#}#!J#Wu+4`U_b z4y{g}{N`1%EZu{7vW{BGcq^{Ldk9(Q)i-EYNeJT36lx@!whe2dAxTw!2 zEF3)`N?8)5Hyy);r8T`4M`vQwlPS0v@0*{iV^gEI|66o3zOH!Wv~+A*RF_l5GIU+F z^SffFnDAFBY{RaB@=U3^eBk}I*krVQe}Ac56T>DtwqjlzR4JFP?#T zPmJdANNkj7O8giO_m24E9Oss`tI>82vj);fA!%sO9%8%e0!vK-xw8OQ&!Z*_{DD=^eHVjmRmlBxd4$RiCMRXHM-eDjoy{-D-)+@ z8$Q)l_Vn#v`5Fn8 zrzcUJU$G@*yI)2Ua6XGAu9REU2d4T%JEzu}O8JPIjrO>e!lIbvizHw?OIRuWXvZkO z&M_xzSq~@D3PG(O(^Gu{63-o$T&07$(Dl;N2+S4r=Ov1=)~_XlUyKAxWA`z{6n%*H zelwYSze?pbph#rxQQs}Ec!EULd7bP^?0SzZ3 z{x+#r^_;+*lzidA%E0d)F`)E0L7V5{|?I#X^NHt2+sdB=v3bXM>hLn9UaTFtK8xWn8Q+p^lcv!A@#6x! zuXlkH%WOkQuh#-GnAFw7E9ntb^kF$n&}hWDmWIyCEtQVL*PT04e zu%qg$h;m5#KSP)ERcIWq!99atCeBR7W3bC}rLe~PU|72ECo;y1@eD-)c$oDv+*6-H z^iQFW#lwSBNyIWhldtF72k3-LPm^Aj8edOd`~Z;y_ku1xO6gSuXDqQqEk= zz-xqC#qc%$*6QJ8+v-&o(+=dX--B7bfCRA{6ha7D3y)@Mj>Nj%rS3s(Q*WV?N4nKO z|1u3dW{^47ieQk{Y9xjl*fp@7kn*H`_K#8lMsj-I{t|U|9i>uL>k>=U0pkM&L@8Qb zlt(tt|G*G}1!eFXfg%VX*Jvk^EwZ3_y&6HFf!l$4B))RGb`BTLAoemTstG;~bnt{2Z&vGwMD$mX$M4lr0kzUCl&3BLp zv`0YUE+(S85>QtAig|u)KyT(o4NK+ z&?7jUkXp{Lo{D{bvH};uC3ZZXP#6WV)t3oK|9P$g>0fnsa?PRN%3f+nxstfvw6caT z(f((N1J7|03ZMtK7qljJ}hxye3Uy)7pK5P84 zRu3mj&ar9OL5wqdghO=a-bUQH0y@?@!s$jhChs!h#~^H3y2?9Q|IFfYm)UL!``VMq zZawj!A4j<9mp=Hzh?j`4=0bEg@E=15?rHN3;WRq1K9tiX>cuV;C)>JPCF%f|v6Hy{ zB`T4c3F$JnNlDTMY*~S0h_If5$coq-XCSB@^0m8?_PkrvQ|*?)h+?MQ;*^0^DF%KI z{2gvZtgR;zGN`|haTsxDBt+gKQiyfWU`ctJsCkk=P9qR^K8LtxoOf69{0>XX4x;UT z9`uykgSfK=;vPt(D~onxNl`>YC;^nt+KPAs%NR%9b5|;zh9zYd(Gs-sh*LV^ z4&-L0dK1+47jT2Po4nl;h`0k9@ice4F${6X4w5%$x621(yWMgi?sNdj8}ysxgR$Ra zB_r<40Ljz)trEi6Z?$G1?mPjDjFvZ$61Kd7^(5lX^B{Shs5o77u#|LU%G+2y5O-dN zg+`9fEXPtp&T$+AlRAmGb2t+sPsv;M$5KKL$vg>i${c{WE7&nm&)^QW-u{(%69?yb`AA@l# z$)Jo|Rd0vI`?rV*@UOzozrnbnTx2}MHf|U=8VR5QECX*aZYvj|dEcYcB_5X;9?k---pW$0A+O_m)+d0}JveatNmS5iAXg{QW@S=QhHoj#n)LJ~Juv zHVdU`J<{fFu7+vp--_Ey>{Vd@+BSsr8XbhCfHrS-gbn5uI2s89+&^tknYX-Cipodr zWS+O|5V`UI)`hXC^Tv-zv71I)MFipcGz}=W0tuusbS64&-u|uTSU8c2jN!-zpjfKV zt&g?ufuZP+QpEZSzmMumnX>V#4mA5-?CR+o;AKy}Wn8!N7TD}1B$SIkK%l=p9 z0Q+Wq%{(L&^Ij8!eKYd2SR7Z#vv;{qE0*9gelIrKx8(mbVxUW1^#IjvpW(hG3z<4D zn|zQo)Qw~Lk!7tn-Z!RkuU&JEcW77Ha~Jn&%y-|I)E;$69!lBL+hlgrt9rB;)Q}#xN~Cdx;mCck7x3%6k!*|z!^{NbAK{cBs!GV`&`u5; zf~2E-JbSE`9=)&+kA3s{EaI|n-UJz63a%NS?U#c#J;Azf;S-$pE$rG1$;c62vn}-K zMc{oSPkqa6-$+LSl9IYN`Q>QzkoLbk^?a|9iSGM$CXVd&{4TEhb_RVZ>+A42zr|d! z`3FyFzByWLwx)33H}zY2UwWfzy(ujf`1FX|$iy@qIP{CZe(#psA`ozH zTAe0ZAg-iMfWHlFksJT}gcr^XY>NH^a5cMF+$5pFnK$yc&0m|mAtgM>FI5j)dCf%; gQ!B&tRKK9`lnu#i=Wok@<4l8uCdHSV$ + + + From d77177bbfd60f181c5db9593b49e7b54f3a94c0c Mon Sep 17 00:00:00 2001 From: ztimms73 Date: Sat, 10 Jul 2021 22:46:58 +0300 Subject: [PATCH 003/180] Change some colors of dark theme --- app/src/main/res/values-night/colors.xml | 5 +++-- app/src/main/res/values/colors.xml | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 2c88fd755..040d450ad 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,8 +1,8 @@ - #4098EF - #2C7CD6 + #42A5F5 + #1565C0 @android:color/black #2EFFFFFF @@ -16,5 +16,6 @@ #1fffffff + #CF6679 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6147fed95..0f940d8bd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,7 +6,7 @@ @android:color/white #39000000 - #F8F9FA + #FFFFFF #B3FFFFFF @@ -14,7 +14,8 @@ @color/system_ui_scrim_black - #1f000000> + #1f000000 + #B00020 @@ -24,7 +25,6 @@ #424242 #212121 #99000000 - #D32F2F #99000000 \ No newline at end of file From dc46657fa69b40a61c65a03effb24f57d429169b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 12 Jul 2021 07:49:30 +0200 Subject: [PATCH 004/180] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ --- app/src/main/res/values-be/strings.xml | 417 ++++++++++++------------- app/src/main/res/values-es/strings.xml | 412 ++++++++++++------------ 2 files changed, 414 insertions(+), 415 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index f5a8f2613..0eaea1d8a 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -1,212 +1,211 @@ - Зачыніць меню - Адкрыць меню - На прыладзе - Абраныя - Гісторыя - Адбылася памылка - Памылка сеткі - Падрабязнасцi - Часткi - Спіс - Падрабязны спіс - Табліца - Выгляд спісу - Наладжвання - Онлайн каталогі - Загрузка… - Частка %1$d з %2$d - Зачыніць - Паўтарыць - Ачысціць гісторыю - Нічога не знойдзена - Гісторыя пустая - Чытаць - Дадаць закладку - Дадайце цікавую для вас мангу ў выбранае, каб не страціць яе - Дадаць у абранае - Стварыць катэгорыю - Дадаць - Увядзіце назву - Захаваць - Падзяліцца - Стварыць ярлык… - Падзялiцца %s - Пошук - Пошук мангі - Загрузка мангі… - Апрацоўка… - Загрузка завершана - Загрузкi - Па імя - Папулярная - Абноўленая - Новая - Па рэйтынгу - Усе - Сартаванне - Жанр - Фільтр - Тэма - Светлая - Цёмная - Аўтаматычна - Старонкi - Ачысціць - Вы ўпэўненыя, што жадаеце ачысціць гісторыю? Гэта дзеянне нельга будзе адмяніць. - Выдаліць - \"%s\" выдалена з гiсторыi - \"%s\" выдалена з прылады - Дачакайцеся заканчэння загрузкі - Захаваць старонку - Старонка захавана - Падзяліцца выявай - Імпарт - Выдаліць - Аперацыя не падтрымліваецца - Падтрымліваюцца толькі ZIP файлы і CBZ. - Няма апісання - Гісторыя і кэш - Ачысціць кэш старонак - Кэш - Б|кБ|МБ|ГБ|ТБ - Стандартны - Манхва - Рэжым чытання - Памер табліцы - Пошук па %s - Выдаліць мангу - Настаўленні чытання - Гартанне старонак - Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады? Гэта дзеянне нельга будзе адмяніць. - Націску па краях - Кнопкі гучнасці - Прадоўжыць - Папярэджанне - Дадзеная аперацыя можа прывесці да вялікага выдатку трафіку - Больш не пытацца - Адмена… - Памылка - Ачысціць кэш мініяцюр - Гісторыя пошуку ачышчана - Ачысціць гісторыю пошуку - Толькі жэсты - Унутраны назапашвальнік - Знешняе сховішча - Дамен - Правяраць абнаўленне прыкладання - Даступна абнаўленне прыкладання - Паказваць апавяшчэнне пры наяўнасці новай версіі - Адкрыць у браўзэры - У гэтай манге %s. Вы ўпэўненыя, што хочаце захаваць іх усё? - Захаваць мангу - Паведамлення - Уключана %1$d з %2$d - Новыя часткi - Апавяшчаць аб абнаўленні мангі, якую вы чытаеце - Загрузіць - Чытаць з пачатку - Перазапусціць - Налады апавяшчэнняў - Гук апавяшчэння - Светлавая iндыкацыя - Вібрацыя - Катэгорыі абранага - Катэгорыi… - Перайменаваць - Вы ўпэўненыя, што хочаце выдаліць катэгорыю \"%s\"? Уся манга з дадзенай катэгорыі будзе страчана. - Выдаліць катэгорыю - Катэгорыі дапамагаюць парадкаваць выбраную мангу. Націсніце «+», каб стварыць катэгорыю - Тут будзе оборажаться манга, якую вы чытаеце. Вы можаце знайсці, што пачытаць, у бакавым меню - У вас пакуль няма захаванай мангі. Вы можаце захаваць мангу з онлайн каталога або імпартаваць з файла - Полка з мангай - Нядаўняя манга - Анімацыя гартання - Месца захавання мангі - Недаступна - Не атрымалася знайсці ні аднаго даступнага сховішчы - Іншае сховішча - Абароненае злучэнне (HTTPS) - Гатова - Усё выбранае - У гэтай катэгорыі нічога няма - Прачытаць пазней - Абнаўлення - Тут будуць адлюстроўвацца абнаўлення мангі, якую вы чытаеце - Вынікі пошуку - Падобныя - Новая версія: %s - Памер: %s - Чаканне падлучэння… - Ачысціць стужку абнаўленняў - Стужка абнаўленняў ачышчана - Павярнуць экран - Абнавіць - Абнаўленне хутка пачнецца - Правяраць абнаўлення мангі - Не правяраць - Увядзіце пароль - Няправільны пароль - Абараніць прыкладанне - Запытваць пароль пры запуску прыкладання - Паўтарыце пароль - Паролі не супадаюць - Аб праграме - Версія %s - Праверыць абнаўлення - Праверка абнаўлення… - Памылка пры праверцы абнаўлення - Няма даступных абнаўленняў - Справа налева - Аддаваць перавагу рэжым Справа налева - Вы можаце наладзіць рэжым чытання для кожнай мангі асобна - Стварыць катэгорыю - Стварыць праблему на GitHub - Маштабаванне - Ўпісаць у экран - Падагнаць па вышыні - Падагнаць па шырыні - Зыходны памер - Чорная цёмная тэма - Карысна для AMOLED экранаў - Патрабуецца перазапуск - Рэзервовае капіраванне - Стварыць рэзервовую копію - Аднавіць дадзеныя - Дадзеныя адноўлены - Падрыхтоўка… - Файл не знойдзены - Усе дадзеныя паспяхова адноўлены - Дадзеныя адноўлены, але ўзніклі некаторыя памылкі - Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх - Толькі што - Учора - Даўно - Групаваць - Сёння - Паспрабаваць яшчэ раз - Абраны рэжым будзе захаваны для бягучай мангі - Без гуку - Неабходна прайсці CAPTCHA - Прайсці - Ачысціць кукi - Усе кукi выдалены - Праверка новых частак: %1$d з %2$d - Ачысціць стужку - Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? - Праверка новых частак - У зваротным парадку - Увайсці - Для прагляду гэтага кантэнту патрабуецца аўтарызацыя - Прадвызначаны: %s - …і яшчэ %1$d - Далей - Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску прыкладання - Пацвярджаць - Пароль павінен змяшчаць не менш за 4 сімвалаў - Схаваць загаловак пры прагортцы - Пошук толькі па %s - Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты? Гэта дзеянне не можа быць адменена. - Апісанне + Зачыніць меню + Адкрыць меню + На прыладзе + Абраныя + Гісторыя + Адбылася памылка + Памылка сеткі + Падрабязнасцi + Часткi + Спіс + Падрабязны спіс + Табліца + Выгляд спісу + Наладжвання + Онлайн каталогі + Загрузка… + Частка %1$d з %2$d + Зачыніць + Паўтарыць + Ачысціць гісторыю + Нічога не знойдзена + Гісторыя пустая + Чытаць + Дадаць закладку + Дадайце цікавую для вас мангу ў выбранае, каб не страціць яе + Дадаць у абранае + Стварыць катэгорыю + Дадаць + Увядзіце назву + Захаваць + Падзяліцца + Стварыць ярлык… + Падзялiцца %s + Пошук + Пошук мангі + Загрузка мангі… + Апрацоўка… + Загрузка завершана + Загрузкi + Па імя + Папулярная + Абноўленая + Новая + Па рэйтынгу + Усе + Сартаванне + Жанр + Фільтр + Тэма + Светлая + Цёмная + Аўтаматычна + Старонкi + Ачысціць + Вы ўпэўненыя, што жадаеце ачысціць гісторыю? Гэта дзеянне нельга будзе адмяніць. + Выдаліць + \"%s\" выдалена з гiсторыi + \"%s\" выдалена з прылады + Дачакайцеся заканчэння загрузкі + Захаваць старонку + Старонка захавана + Падзяліцца выявай + Імпарт + Выдаліць + Аперацыя не падтрымліваецца + Падтрымліваюцца толькі ZIP файлы і CBZ. + Няма апісання + Гісторыя і кэш + Ачысціць кэш старонак + Кэш + Б|кБ|МБ|ГБ|ТБ + Стандартны + Манхва + Рэжым чытання + Памер табліцы + Пошук па %s + Выдаліць мангу + Настаўленні чытання + Гартанне старонак + Вы ўпэўненыя, што жадаеце выдаліць \"%s\" з прылады? Гэта дзеянне нельга будзе адмяніць. + Націску па краях + Кнопкі гучнасці + Прадоўжыць + Папярэджанне + Дадзеная аперацыя можа прывесці да вялікага выдатку трафіку + Больш не пытацца + Адмена… + Памылка + Ачысціць кэш мініяцюр + Гісторыя пошуку ачышчана + Ачысціць гісторыю пошуку + Толькі жэсты + Унутраны назапашвальнік + Знешняе сховішча + Дамен + Правяраць абнаўленне прыкладання + Даступна абнаўленне прыкладання + Паказваць апавяшчэнне пры наяўнасці новай версіі + Адкрыць у браўзэры + У гэтай манге %s. Вы ўпэўненыя, што хочаце захаваць іх усё? + Захаваць мангу + Паведамлення + Уключана %1$d з %2$d + Новыя часткi + Апавяшчаць аб абнаўленні мангі, якую вы чытаеце + Загрузіць + Чытаць з пачатку + Перазапусціць + Налады апавяшчэнняў + Гук апавяшчэння + Светлавая iндыкацыя + Вібрацыя + Катэгорыі абранага + Катэгорыi… + Перайменаваць + Вы ўпэўненыя, што хочаце выдаліць катэгорыю \"%s\"? Уся манга з дадзенай катэгорыі будзе страчана. + Выдаліць катэгорыю + Катэгорыі дапамагаюць парадкаваць выбраную мангу. Націсніце «+», каб стварыць катэгорыю + У вас пакуль няма захаванай мангі. Вы можаце захаваць мангу з онлайн каталога або імпартаваць з файла + Полка з мангай + Нядаўняя манга + Анімацыя гартання + Месца захавання мангі + Недаступна + Не атрымалася знайсці ні аднаго даступнага сховішчы + Іншае сховішча + Абароненае злучэнне (HTTPS) + Гатова + Усё выбранае + У гэтай катэгорыі нічога няма + Прачытаць пазней + Абнаўлення + Тут будуць адлюстроўвацца абнаўлення мангі, якую вы чытаеце + Вынікі пошуку + Падобныя + Новая версія: %s + Памер: %s + Чаканне падлучэння… + Ачысціць стужку абнаўленняў + Стужка абнаўленняў ачышчана + Павярнуць экран + Абнавіць + Абнаўленне хутка пачнецца + Правяраць абнаўлення мангі + Не правяраць + Увядзіце пароль + Няправільны пароль + Абараніць прыкладанне + Запытваць пароль пры запуску прыкладання + Паўтарыце пароль + Паролі не супадаюць + Аб праграме + Версія %s + Праверыць абнаўлення + Праверка абнаўлення… + Памылка пры праверцы абнаўлення + Няма даступных абнаўленняў + Справа налева + Аддаваць перавагу рэжым Справа налева + Вы можаце наладзіць рэжым чытання для кожнай мангі асобна + Стварыць катэгорыю + Стварыць праблему на GitHub + Маштабаванне + Ўпісаць у экран + Падагнаць па вышыні + Падагнаць па шырыні + Зыходны памер + Чорная цёмная тэма + Карысна для AMOLED экранаў + Патрабуецца перазапуск + Рэзервовае капіраванне + Стварыць рэзервовую копію + Аднавіць дадзеныя + Дадзеныя адноўлены + Падрыхтоўка… + Файл не знойдзены + Усе дадзеныя паспяхова адноўлены + Дадзеныя адноўлены, але ўзніклі некаторыя памылкі + Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх + Толькі што + Учора + Даўно + Групаваць + Сёння + Паспрабаваць яшчэ раз + Абраны рэжым будзе захаваны для бягучай мангі + Без гуку + Неабходна прайсці CAPTCHA + Прайсці + Ачысціць кукi + Усе кукi выдалены + Праверка новых частак: %1$d з %2$d + Ачысціць стужку + Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? + Праверка новых частак + У зваротным парадку + Увайсці + Для прагляду гэтага кантэнту патрабуецца аўтарызацыя + Прадвызначаны: %s + …і яшчэ %1$d + Далей + Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску прыкладання + Пацвярджаць + Пароль павінен змяшчаць не менш за 4 сімвалаў + Схаваць загаловак пры прагортцы + Пошук толькі па %s + Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты? Гэта дзеянне не можа быць адменена. + Апісанне \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 071f78ac5..2a8b22b08 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,207 +1,207 @@ + - Cerrar menú - Abrir menú - Dispositivo - Favoritos - Historial - Ocurrió un error - Error de red - Detalles - Capítulos - Lista - Lista detallada - Cuadrículas - Vista de lista - Ajustes - Fuentes remotas - Cargando… - Capítulo %1$d de %2$d - Cerrar - Reintentar - Borrar historial - No se encontró nada - Historial vacío - Leer - Añadir marcador - Añade tu manga favorito primero - Añadir a favoritos - Añadir categoría - Añadir - Introduce categoría - Guardar - Compartir - Crear acceso directo… - Compartir %s - Buscar - Buscar manga - Descargar manga… - Procesando… - Descarga completa - Descargas - Por nombre - Popularidad - Actualización - Recientes - Por calificación - Todo - Establecer orden - Género - Filtrar - Tema - Claro - Oscuro - Automático - Páginas - Borrar - ¿Realmente quieres borrar todo tu historial de lectura? Esta acción no se puede deshacer. - Eliminar - \"%s\" retirado del historial - \"%s\" borrado del almacenamiento local - Espera que termine la carga - Guardar página - Página guardada con éxito - Compartir imagen - Importar - Borrar - Esta operación no está admitida - Archivo no válido. Sólo se admiten ZIP y CBZ. - Sin descripción - Historial y caché - Borrar la caché de páginas - Caché - B|kB|MB|GB|TB - Estándar - Webtoon - Modo de lectura - Tamaño de la cuadrícula - Buscar en %s - Borrar manga - ¿Realmente quieres borrar \"%s\" del almacenamiento local de tu teléfono? \nEsta operación no se puede deshacer. - Ajustes del lector - Cambiar de página - Tapas en los bordes - Botones de volumen - Continuar - Advertencia - Esta operación puede consumir mucho tráfico de red - No volver a preguntar - Cancelar… - Error - Borrar la caché de miniaturas - Borrar el historial de búsqueda - Historial de búsqueda borrado - Sólo gestos - Almacenamiento interno - Almacenamiento externo - Dominio - Comprobar actualizaciones automáticamente - Una nueva versión de la aplicación está disponible - Mostrar notificación si la actualización está disponible - Abrir en el navegador - Este manga tiene %s. ¿Quieres guardarlo todo? - Guardar manga - Notificaciones - Activado %1$d de %2$d - Nuevos capítulos - Notificar sobre las actualizaciones del manga que estás leyendo - Descargar - Leer desde el principio - Reiniciar - Configuración de las notificaciones - Sonido de las notificaciones - Indicador de luz - Vibración - Categorías favoritas - Categorías… - Renombrar - ¿Realmente quieres eliminar la categoría \"%s\" de tus favoritos? \nTodo el manga en ella se perderá. - Quitar categoría - Puedes usar categorías para organizar tus mangas favoritos. Pulsa «+» para crear una categoría - El manga que estás leyendo se mostrará aquí. Puedes encontrar qué leer en el menú lateral - Todavía no tienes ningún manga guardado. Puedes guardarlo desde fuentes remotas o importarlo desde un archivo - Estante de manga - Manga reciente - Animación de páginas - Ubicación de descarga del manga - No disponible - No se puede encontrar ningún almacenamiento disponible - Otro almacenamiento - Utilizar conexión segura (HTTPS) - Aceptar - Todos los favoritos - Esta categoría está vacía - Leer más tarde - Actualizaciones - Aquí verás los nuevos episodios del manga que estás leyendo - Resultados de la búsqueda - Relación - Nueva versión: %s - Tamaño: %s - Esperando red… - Borrar actualizaciones - Borrar el feed de actualizaciones - Girar pantalla - Actualizar - La actualización de la alimentación comenzará pronto - Comprueba las actualizaciones del manga - No comprobar - Introducir contraseña - Contraseña incorrecta - Proteger aplicación - Pide la contraseña al iniciar la aplicación - Repite la contraseña - Las contraseñas no coinciden - Acerca de - Versión %s - Comprobar actualizaciones - Comprobar si hay actualizaciones… - Fallo en la comprobación de actualizaciones - No hay actualizaciones disponibles - Derecha a izquierda - Preferir lector de derecha a izquierda - Puedes configurar el modo de lectura para cada manga por separado - Nueva categoría - Crear incidencia en GitHub - Modo de escala - Ajuste al centro - Ajuste a la altura - Ajuste a la anchura - Mantener al iniciar - Tema oscuro auténtico - Útil para pantallas AMOLED - Se requiere reinicio - - Crear copia de seguridad de datos - Restaurar desde la copia de seguridad - Datos restaurados - Preparando… - Archivo no encontrado - Todos los datos fueron restaurados con éxito - Los datos fueron restaurados, pero hay errores - Puedes crear una copia de seguridad de tu historial y favoritos para restaurarla - Ahora mismo - Ayer - Hace mucho tiempo - Grupo - Hoy - Toca para volver a intentar - Se recordará la configuración elegida para este manga - Silenciar - El captcha es obligatorio - Resolver - Borrar cookies - Se han eliminado todas las cookies - Buscando nuevos capítulos: %1$d de %2$d - Limpiar feed - Todo el historial de actualizaciones se borrará y esta acción no se puede deshacer. ¿Está seguro? - Comprobación de nuevos capítulos - Invertir - Iniciar sesión - Debes autorizar para ver este contenido - Por defecto: %s - …y %1$d más - Siguiente - Introduce la contraseña que se requerirá cuando se inicie la aplicación - Confirmar - La contraseña debe tener al menos 4 caracteres - + Cerrar menú + Abrir menú + Dispositivo + Favoritos + Historial + Ocurrió un error + Error de red + Detalles + Capítulos + Lista + Lista detallada + Cuadrículas + Vista de lista + Ajustes + Fuentes remotas + Cargando… + Capítulo %1$d de %2$d + Cerrar + Reintentar + Borrar historial + No se encontró nada + Historial vacío + Leer + Añadir marcador + Añade tu manga favorito primero + Añadir a favoritos + Añadir categoría + Añadir + Introduce categoría + Guardar + Compartir + Crear acceso directo… + Compartir %s + Buscar + Buscar manga + Descargar manga… + Procesando… + Descarga completa + Descargas + Por nombre + Popularidad + Actualización + Recientes + Por calificación + Todo + Establecer orden + Género + Filtrar + Tema + Claro + Oscuro + Automático + Páginas + Borrar + ¿Realmente quieres borrar todo tu historial de lectura? Esta acción no se puede deshacer. + Eliminar + \"%s\" retirado del historial + \"%s\" borrado del almacenamiento local + Espera que termine la carga + Guardar página + Página guardada con éxito + Compartir imagen + Importar + Borrar + Esta operación no está admitida + Archivo no válido. Sólo se admiten ZIP y CBZ. + Sin descripción + Historial y caché + Borrar la caché de páginas + Caché + B|kB|MB|GB|TB + Estándar + Webtoon + Modo de lectura + Tamaño de la cuadrícula + Buscar en %s + Borrar manga + ¿Realmente quieres borrar \"%s\" del almacenamiento local de tu teléfono? \nEsta operación no se puede deshacer. + Ajustes del lector + Cambiar de página + Tapas en los bordes + Botones de volumen + Continuar + Advertencia + Esta operación puede consumir mucho tráfico de red + No volver a preguntar + Cancelar… + Error + Borrar la caché de miniaturas + Borrar el historial de búsqueda + Historial de búsqueda borrado + Sólo gestos + Almacenamiento interno + Almacenamiento externo + Dominio + Comprobar actualizaciones automáticamente + Una nueva versión de la aplicación está disponible + Mostrar notificación si la actualización está disponible + Abrir en el navegador + Este manga tiene %s. ¿Quieres guardarlo todo? + Guardar manga + Notificaciones + Activado %1$d de %2$d + Nuevos capítulos + Notificar sobre las actualizaciones del manga que estás leyendo + Descargar + Leer desde el principio + Reiniciar + Configuración de las notificaciones + Sonido de las notificaciones + Indicador de luz + Vibración + Categorías favoritas + Categorías… + Renombrar + ¿Realmente quieres eliminar la categoría \"%s\" de tus favoritos? \nTodo el manga en ella se perderá. + Quitar categoría + Puedes usar categorías para organizar tus mangas favoritos. Pulsa «+» para crear una categoría + Todavía no tienes ningún manga guardado. Puedes guardarlo desde fuentes remotas o importarlo desde un archivo + Estante de manga + Manga reciente + Animación de páginas + Ubicación de descarga del manga + No disponible + No se puede encontrar ningún almacenamiento disponible + Otro almacenamiento + Utilizar conexión segura (HTTPS) + Aceptar + Todos los favoritos + Esta categoría está vacía + Leer más tarde + Actualizaciones + Aquí verás los nuevos episodios del manga que estás leyendo + Resultados de la búsqueda + Relación + Nueva versión: %s + Tamaño: %s + Esperando red… + Borrar actualizaciones + Borrar el feed de actualizaciones + Girar pantalla + Actualizar + La actualización de la alimentación comenzará pronto + Comprueba las actualizaciones del manga + No comprobar + Introducir contraseña + Contraseña incorrecta + Proteger aplicación + Pide la contraseña al iniciar la aplicación + Repite la contraseña + Las contraseñas no coinciden + Acerca de + Versión %s + Comprobar actualizaciones + Comprobar si hay actualizaciones… + Fallo en la comprobación de actualizaciones + No hay actualizaciones disponibles + Derecha a izquierda + Preferir lector de derecha a izquierda + Puedes configurar el modo de lectura para cada manga por separado + Nueva categoría + Crear incidencia en GitHub + Modo de escala + Ajuste al centro + Ajuste a la altura + Ajuste a la anchura + Mantener al iniciar + Tema oscuro auténtico + Útil para pantallas AMOLED + Se requiere reinicio + + Crear copia de seguridad de datos + Restaurar desde la copia de seguridad + Datos restaurados + Preparando… + Archivo no encontrado + Todos los datos fueron restaurados con éxito + Los datos fueron restaurados, pero hay errores + Puedes crear una copia de seguridad de tu historial y favoritos para restaurarla + Ahora mismo + Ayer + Hace mucho tiempo + Grupo + Hoy + Toca para volver a intentar + Se recordará la configuración elegida para este manga + Silenciar + El captcha es obligatorio + Resolver + Borrar cookies + Se han eliminado todas las cookies + Buscando nuevos capítulos: %1$d de %2$d + Limpiar feed + Todo el historial de actualizaciones se borrará y esta acción no se puede deshacer. ¿Está seguro? + Comprobación de nuevos capítulos + Invertir + Iniciar sesión + Debes autorizar para ver este contenido + Por defecto: %s + …y %1$d más + Siguiente + Introduce la contraseña que se requerirá cuando se inicie la aplicación + Confirmar + La contraseña debe tener al menos 4 caracteres + \ No newline at end of file From 75b9fd1b7aef8164ede68064a8ee761e1bf4a866 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 12 Jul 2021 07:52:30 +0200 Subject: [PATCH 005/180] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ --- app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 0eaea1d8a..df1cb5b31 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -118,7 +118,6 @@ Вы ўпэўненыя, што хочаце выдаліць катэгорыю \"%s\"? Уся манга з дадзенай катэгорыі будзе страчана. Выдаліць катэгорыю Катэгорыі дапамагаюць парадкаваць выбраную мангу. Націсніце «+», каб стварыць катэгорыю - У вас пакуль няма захаванай мангі. Вы можаце захаваць мангу з онлайн каталога або імпартаваць з файла Полка з мангай Нядаўняя манга Анімацыя гартання diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2a8b22b08..4771f1165 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -118,7 +118,6 @@ ¿Realmente quieres eliminar la categoría \"%s\" de tus favoritos? \nTodo el manga en ella se perderá. Quitar categoría Puedes usar categorías para organizar tus mangas favoritos. Pulsa «+» para crear una categoría - Todavía no tienes ningún manga guardado. Puedes guardarlo desde fuentes remotas o importarlo desde un archivo Estante de manga Manga reciente Animación de páginas From 8ae78631857a9bec87671826c1ca9c83fbbcd4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Mon, 12 Jul 2021 14:15:12 +0200 Subject: [PATCH 006/180] =?UTF-8?q?Added=20translation=20using=20Weblate?= =?UTF-8?q?=20(Norwegian=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values-nb-rNO/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-nb-rNO/strings.xml diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 7402e8569af12060196d57cb62bf54a9461d7345 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 12 Jul 2021 15:43:22 +0200 Subject: [PATCH 007/180] Added translation using Weblate (German) --- app/src/main/res/values-de/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-de/strings.xml diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From eba5e484d617d032310a0f098e449bdcc993c307 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 12 Jul 2021 15:45:40 +0200 Subject: [PATCH 008/180] Added translation using Weblate (Italian) --- app/src/main/res/values-it/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-it/strings.xml diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 15d094a1754c56ccdef610f40d82573a5f1ad415 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 12 Jul 2021 18:03:00 +0200 Subject: [PATCH 009/180] Added translation using Weblate (Portuguese) --- app/src/main/res/values-pt/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-pt/strings.xml diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From eb780a1449fbae6bd02f62c60210245ede0a517c Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 12 Jul 2021 18:04:01 +0200 Subject: [PATCH 010/180] Added translation using Weblate (French) --- app/src/main/res/values-fr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-fr/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 384d0345f5ef946e374f3d84a5033e6ffc349931 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 12 Jul 2021 12:01:27 +0000 Subject: [PATCH 011/180] Translated using Weblate (Belarusian) Currently translated at 100.0% (221 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ --- app/src/main/res/values-be/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index df1cb5b31..54fcffb50 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -207,4 +207,16 @@ Пошук толькі па %s Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты? Гэта дзеянне не можа быць адменена. Апісанне + Падрабязна + Некаторыя вытворцы могуць змяняць паводзіны сістэмы, што можа парушаць выкананне фонавых задач. + Рэзервовая копія паспяхова захавана + Вітаю + Мовы + Іншыя + Вы можаце захаваць яго з онлайн-крыніц або імпартаваць з файла. + У Вас яшчэ няма ні адной захаванай мангі + Вы можаце знайсці, што пачытаць у бакавым меню. + Тут будзе адлюстроўвацца манга, якую вы чытаеце + Паспрабуйце перафармуляваць запыт. + Тут неяк пуста… \ No newline at end of file From 96d6f9d80d0d61472c5fcd706846112225be69d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Mon, 12 Jul 2021 13:59:17 +0000 Subject: [PATCH 012/180] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 86.8% (192 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 222 ++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a6b3daec9..f0f089cbf 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -1,2 +1,222 @@ - \ No newline at end of file + + B|kB|MB|GB|TB + Finner ikke noen tilgjengelig lagring + Nedlastingssted + Du kan lagre fra nettbaserte kilder, eller importere filer. + Noen enheter endrer systemoppførselen, noe som kan ødelegge for bakgrunnsoppgaver. + Fjern alle nylige søkespørringer for godt\? + Skjul verktøylinje under rulling + Passord må være minst fire tegn + Skriv inn passord å kreve for å starte programmet + … og %1$d til + Autoriser visning av innholdet + Inverter + Sjekk etter nye kapitler + Tøm all oppdateringshistorikk for godt\? + Tøm flyt + Ser etter nye kapitler: %1$d av %2$d + Valgt oppsett vil bli husket for denne filen + Du kan opprette en sikkerhetskopi av din historikk og favoritter å gjenopprette senere + Data gjenopprettet, men med feil + Fant ikke filen + Sikkerhetskopiering og gjenoppretting + Behold ved oppstart + Tilpass bredde + Tilpass høyde + Tilpass sentrum + Skaleringsmodus + Opprett feilrapport på GitHub + Du kan sette opp lesemodus for hver fil + Høyre-til-venstre ← + Foretrekk høyre-til-venstre -leser + Ingen tilgjengelige oppdateringer + Kunne ikke se etter oppdateringer + Ser etter oppdateringer … + Se etter oppdateringer + Passord ved programoppstart + Se etter leseoppdateringer + Flytoppdatering starter snart + Oppdater + Tømt + Tøm oppdateringsflyt + Venter på nettverkstilknytning … + Ny versjon: %s + Her kan du se nye kapitler av det du leser + Oppdatering + Bruk sikker (HTTPS)-tilkobling + Annen lagring + Hylle + Du har ikke lagret noe enda. + Du kan finne ting å lese i sidemenyen. + Det du leser vil vises her + Prøv å reformulere spørringen. + Bruk kategorier til å organisere mapper. Trykk «+» for å opprette en kategori. + Det er ganske tomt her … + Fjern «%s»-kategorien fra favoritter og alt i den\? + Programomstart + Gi merknad om oppdateringer av det du leser + Skrudde på %1$d av %2$d + Denne managaen har %s. Ønsker du å lagre hele\? + Åpne i filutforsker + Vis merknad hvis ny versjon er tilgjengelig + Ny programversjon tilgjengelig + Se etter oppdateringer automatisk + Søkehistorikk tømt + Tøm miniatyrbildehurtiglager + Kan laste ned mye data + Trykk på kanter + Slett «%s» fra enhetsminne for godt\? + Nettserie + Forvalg + Hurtiglager + Tøm sidehurtiglager + Historikk og hurtiglager + Velg en ZIP- eller CBZ-fil. + Ustøttet handling + Importer + Lagret + Vent på at innlastingen fullføres + Fjern all lesehistorikk for godt\? + Les + Resultatløst + Kilder annensteds hen + Les mer + Sikkerhetskopi lagret + Beskrivelse + Velkommen + Språk + Annet + Kun søk på %s + Bekreft + Neste + Forvalg: %s + Logg inn + Alle kaker ble fjernet + Tøm kaker + Løs + CAPTCHA kreves + Stille + Trykk for å prøve igjen + I dag + Gruppe + Lenge siden + I går + Akkurat nå + All data gjenopprettet + Forbereder … + Data gjenopprettet + Gjenopprett fra sikkerhetskopi + Opprett sikkerhetskopi + Omstart kreves + Nyttig for AMOLED-skjermer + Nattsvart drakt + Ny kategori + Versjon %s + Om + Passordene samsvarer ikke + Gjenta passord + Beskytt programmet + Feil passord + Skriv inn passord + Ikke sjekk + Roter skjerm + Størrelse: %s + Relatert + Søkeresultater + Les senere + Denne kategorien er tom + Alle favoritter + Ferdig + Ikke tilgjengelig + Sideanimasjon + Nylig manga + Fjern kategori + Gi nytt navn + Kategorier … + Favorittkategorier + Lysindikator + Vibrasjon + Merknadslyd + Merknadsinnstillinger + Les fra begynnelsen + Last ned + Nye kapittel + Merknader + Lagre manga + Domene + Eksternlagring + Internlagring + Kun håndvendinger + Tøm søkehistorikk + Feil + Avbryter … + Ikke spør igjen + Advarsel + Fortsett + Lydstyrkeknapper + Bytt sider + Leserinnstillinger + Slett manga + Søk på %s + Rutenettsstørrelse + Lesemodus + Ingen beskrivelse + Slett + Del bilde + Lagre side + «%s» slettet fra lokallagring + «%s» fjernet fra historikk + Fjern + Tøm + Sider + Automatisk + Mørk + Lys + Navn + Drakt + Filter + Sjanger + Sorteringsrekkefølge + Alle + Vurdering + Nyeste + Oppdatert + Popularitet + Nedlastinger + Nedlastet + Behandler … + Laster ned manga … + Søk manga + Søk + Del %s + Opprett snarvei … + Del + Lagre + Skriv inn kategorinavn + Legg til + Legg til ny kategori + Legg til i favoritter + Du har ingen favoritter enda. + Legg til bokmerke + Historikken er tom + Tøm historikk + Prøv igjen + Lukk + Kapittel %1$d av %2$d + Laster inn … + Innstillinger + Listemodus + Rutenett + Detaljert liste + Liste + Kapittel + Detaljer + Nettverktilkoblingsfeil + En feil inntraff + Historikk + Favoritter + Lokallagring + Åpne meny + Lukk meny + \ No newline at end of file From 86be3933350b1746e9ba9749305745f5a1881aff Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 12 Jul 2021 21:16:58 +0300 Subject: [PATCH 013/180] Update dependencies --- app/build.gradle | 12 ++++++------ build.gradle | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bf0ec6b6a..ce37ed57b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,10 +62,10 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' - implementation 'androidx.core:core-ktx:1.5.0' + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.activity:activity-ktx:1.2.3' implementation 'androidx.fragment:fragment-ktx:1.3.5' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' @@ -79,7 +79,7 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.6.0-beta01' - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.4.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1' @@ -89,13 +89,13 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'com.squareup.okio:okio:2.10.0' - implementation 'org.jsoup:jsoup:1.13.1' + implementation 'org.jsoup:jsoup:1.14.1' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.0' implementation 'io.insert-koin:koin-android:3.1.2' - implementation 'io.coil-kt:coil-base:1.2.2' + implementation 'io.coil-kt:coil-base:1.3.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.2' diff --git a/build.gradle b/build.gradle index 90a323679..cab96dae5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 6a3421df8a267eeeea52b96b2485168661df18fd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 14 Jul 2021 07:01:11 +0300 Subject: [PATCH 014/180] Adjust nullability in parsers --- .../core/parser/RemoteMangaRepository.kt | 5 ++ .../core/parser/site/AnibelRepository.kt | 52 +++++++++++-------- .../core/parser/site/ChanRepository.kt | 12 ++--- .../core/parser/site/DesuMeRepository.kt | 8 +-- .../core/parser/site/GroupleRepository.kt | 8 +-- .../core/parser/site/HenChanRepository.kt | 2 +- .../core/parser/site/MangaLibRepository.kt | 23 ++++---- .../core/parser/site/MangaTownRepository.kt | 30 ++++++----- .../core/parser/site/MangareadRepository.kt | 25 +++++---- .../core/parser/site/NineMangaRepository.kt | 23 ++++---- .../koitharu/kotatsu/utils/ext/StringExt.kt | 1 + 11 files changed, 105 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index ce91c0ddc..87e7812c8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.parser import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaTag @@ -75,4 +76,8 @@ abstract class RemoteMangaRepository( h = 31 * h + id return h } + + protected fun parseFailed(message: String? = null): Nothing { + throw ParseException(message) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 35c5e6187..82c924435 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -32,30 +32,33 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor else -> "/manga?page=$page".withDomain() } val doc = loaderContext.httpGet(link).parseHtml() - val root = doc.body().select("div.manga-block") ?: throw ParseException("Cannot find root") + val root = doc.body().select("div.manga-block") ?: parseFailed("Cannot find root") val items = root.select("div.anime-card") return items.mapNotNull { card -> - val href = card.selectFirst("a").attr("href") + val href = card.selectFirst("a")?.attr("href") ?: return@mapNotNull null val status = card.select("tr")[2].text() - val fullTitle = card.selectFirst("h1.anime-card-title").text() - .substringBeforeLast('[') + val fullTitle = card.selectFirst("h1.anime-card-title")?.text() + ?.substringBeforeLast('[') ?: return@mapNotNull null val titleParts = fullTitle.splitTwoParts('/') Manga( id = generateUid(href), title = titleParts?.first?.trim() ?: fullTitle, - coverUrl = card.selectFirst("img").attr("data-src").withDomain(), + coverUrl = card.selectFirst("img")?.attr("data-src") + ?.withDomain().orEmpty(), altTitle = titleParts?.second?.trim(), author = null, rating = Manga.NO_RATING, url = href, publicUrl = href.withDomain(), - tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> + tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x -> MangaTag( title = x.text(), - key = x.attr("href")?.substringAfterLast("=") ?: return@tags null, + key = x.attr("href").ifEmpty { + return@mapNotNull null + }.substringAfterLast("="), source = source ) - }.orEmpty(), + }, state = when (status) { "выпускаецца" -> MangaState.ONGOING "завершанае" -> MangaState.FINISHED @@ -68,16 +71,17 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor override suspend fun getDetails(manga: Manga): Manga { val doc = loaderContext.httpGet(manga.publicUrl).parseHtml() - val root = doc.body().select("div.container") ?: throw ParseException("Cannot find root") + val root = doc.body().select("div.container") ?: parseFailed("Cannot find root") return manga.copy( description = root.select("div.manga-block.grid-12")[2].select("p").text(), chapters = root.select("ul.series").flatMap { table -> table.select("li") }.map { it.selectFirst("a") }.mapIndexedNotNull { i, a -> - val href = a.select("a").first().attr("href").toRelativeUrl(getDomain()) + val href = a?.select("a")?.first()?.attr("href") + ?.toRelativeUrl(getDomain()) ?: return@mapIndexedNotNull null MangaChapter( id = generateUid(href), - name = a.select("a").first().text(), + name = a.selectFirst("a")?.text().orEmpty(), number = i + 1, url = href, source = source @@ -112,16 +116,17 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor ) } } - throw ParseException("Pages list not found at ${chapter.url.withDomain()}") + parseFailed("Pages list not found at ${chapter.url.withDomain()}") } override suspend fun getTags(): Set { val doc = loaderContext.httpGet("https://${getDomain()}/manga").parseHtml() val root = doc.body().select("div#tabs-genres").select("ul#list.ul-three-colums") - return root.select("p.menu-tags.tupe").mapToSet { a -> + return root.select("p.menu-tags.tupe").mapToSet { p -> + val a = p.selectFirst("a") ?: parseFailed("a is null") MangaTag( - title = a.select("a").text().capitalize(Locale.ROOT), - key = a.select("a").attr("data-name"), + title = a.text().toCamelCase(), + key = a.attr("data-name"), source = source ) } @@ -130,30 +135,33 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor private suspend fun search(query: String): List { val domain = getDomain() val doc = loaderContext.httpGet("https://$domain/search?q=$query").parseHtml() - val root = doc.body().select("div.manga-block").select("article.tab-2") ?: throw ParseException("Cannot find root") + val root = doc.body().select("div.manga-block").select("article.tab-2") ?: parseFailed("Cannot find root") val items = root.select("div.anime-card") return items.mapNotNull { card -> val href = card.select("a").attr("href") val status = card.select("tr")[2].text() - val fullTitle = card.selectFirst("h1.anime-card-title").text() - .substringBeforeLast('[') + val fullTitle = card.selectFirst("h1.anime-card-title")?.text() + ?.substringBeforeLast('[') ?: return@mapNotNull null val titleParts = fullTitle.splitTwoParts('/') Manga( id = generateUid(href), title = titleParts?.first?.trim() ?: fullTitle, - coverUrl = card.selectFirst("img").attr("src").withDomain(), + coverUrl = card.selectFirst("img")?.attr("src") + ?.withDomain().orEmpty(), altTitle = titleParts?.second?.trim(), author = null, rating = Manga.NO_RATING, url = href, publicUrl = href.withDomain(), - tags = card.select("p.tupe.tag")?.select("a")?.mapNotNullToSet tags@{ x -> + tags = card.select("p.tupe.tag").select("a").mapNotNullToSet tags@{ x -> MangaTag( title = x.text(), - key = x.attr("href")?.substringAfterLast("=") ?: return@tags null, + key = x.attr("href").ifEmpty { + return@mapNotNull null + }.substringAfterLast("="), source = source ) - }.orEmpty(), + }, state = when (status) { "выпускаецца" -> MangaState.ONGOING "завершанае" -> MangaState.FINISHED diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt index e5828c154..242686815 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt @@ -35,7 +35,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset" } val doc = loaderContext.httpGet(url).parseHtml() - val root = doc.body().selectFirst("div.main_fon").getElementById("content") + val root = doc.body().selectFirst("div.main_fon")?.getElementById("content") ?: throw ParseException("Cannot find root") return root.select("div.content_row").mapNotNull { row -> val a = row.selectFirst("div.manga_row1")?.selectFirst("h2")?.selectFirst("a") @@ -78,7 +78,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe chapters = root.select("table.table_cha").flatMap { table -> table.select("div.manga2") }.map { it.selectFirst("a") }.reversed().mapIndexedNotNull { i, a -> - val href = a.relUrl("href") + val href = a?.relUrl("href") ?: return@mapIndexedNotNull null MangaChapter( id = generateUid(href), name = a.text().trim(), @@ -123,12 +123,12 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe override suspend fun getTags(): Set { val domain = getDomain() val doc = loaderContext.httpGet("https://$domain/catalog").parseHtml() - val root = doc.body().selectFirst("div.main_fon").getElementById("side") - .select("ul").last() + val root = doc.body().selectFirst("div.main_fon")?.getElementById("side") + ?.select("ul")?.last() ?: throw ParseException("Cannot find root") return root.select("li.sidetag").mapToSet { li -> - val a = li.children().last() + val a = li.children().last() ?: throw ParseException("a is null") MangaTag( - title = a.text().capitalize(), + title = a.text().toCamelCase(), key = a.attr("href").substringAfterLast('/'), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt index c09530342..4d62b7af5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt @@ -6,7 +6,6 @@ import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.utils.ext.* import java.util.* -import kotlin.collections.ArrayList class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { @@ -122,12 +121,13 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor override suspend fun getTags(): Set { val doc = loaderContext.httpGet("https://${getDomain()}/manga/").parseHtml() - val root = doc.body().getElementById("animeFilter").selectFirst(".catalog-genres") + val root = doc.body().getElementById("animeFilter") + ?.selectFirst(".catalog-genres") ?: throw ParseException("Root not found") return root.select("li").mapToSet { MangaTag( source = source, - key = it.selectFirst("input").attr("data-genre"), - title = it.selectFirst("label").text() + key = it.selectFirst("input")?.attr("data-genre") ?: parseFailed(), + title = it.selectFirst("label")?.text() ?: parseFailed() ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt index b680b5cf0..268ad03b0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt @@ -57,7 +57,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : if (descDiv.selectFirst("i.fa-user") != null) { return@mapNotNull null //skip author } - val href = imgDiv.selectFirst("a").attr("href")?.inContextOf(node) + val href = imgDiv.selectFirst("a")?.attr("href")?.inContextOf(node) if (href == null || href.toHttpUrl().host != baseHost) { return@mapNotNull null // skip external links } @@ -161,11 +161,11 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : override suspend fun getTags(): Set { val doc = loaderContext.httpGet("https://${getDomain()}/list/genres/sort_name").parseHtml() - val root = doc.body().getElementById("mangaBox").selectFirst("div.leftContent") - .selectFirst("table.table") + val root = doc.body().getElementById("mangaBox")?.selectFirst("div.leftContent") + ?.selectFirst("table.table") ?: parseFailed("Cannot find root") return root.select("a.element-link").mapToSet { a -> MangaTag( - title = a.text().capitalize(), + title = a.text().toCamelCase(), key = a.attr("href").substringAfterLast('/'), source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt index e405aed9c..4033584d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt @@ -36,7 +36,7 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load description = root.getElementById("description")?.html()?.substringBeforeLast(" val a = card.selectFirst("a.media-card") ?: return@mapNotNull null val href = a.relUrl("href") Manga( id = generateUid(href), - title = card.selectFirst("h3").text(), + title = card.selectFirst("h3")?.text().orEmpty(), coverUrl = a.absUrl("data-src"), altTitle = null, author = null, @@ -98,10 +98,11 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : append(item.getInt("chapter_volume")) append("/c") append(item.getString("chapter_number")) + @Suppress("BlockingMethodInNonBlockingContext") // lint issue append('/') append(item.optString("chapter_string")) } - var name = item.getString("chapter_name") + var name = item.getStringOrNull("chapter_name") if (name.isNullOrBlank() || name == "null") { name = "Том " + item.getInt("chapter_volume") + " Глава " + item.getString("chapter_number") @@ -128,17 +129,17 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : rating = root.selectFirst("div.media-stats-item__score") ?.selectFirst("span") ?.text()?.toFloatOrNull()?.div(5f) ?: manga.rating, - author = info.getElementsMatchingOwnText("Автор").firstOrNull() + author = info?.getElementsMatchingOwnText("Автор")?.firstOrNull() ?.nextElementSibling()?.text() ?: manga.author, - tags = info.selectFirst("div.media-tags") + tags = info?.selectFirst("div.media-tags") ?.select("a.media-tag-item")?.mapToSet { a -> MangaTag( - title = a.text().capitalize(), + title = a.text().toCamelCase(), key = a.attr("href").substringAfterLast('='), source = source ) } ?: manga.tags, - description = info.selectFirst("div.media-description__text")?.html(), + description = info?.selectFirst("div.media-description__text")?.html(), chapters = chapters ) } @@ -146,11 +147,11 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.withDomain() val doc = loaderContext.httpGet(fullUrl).parseHtml() - if (doc.location()?.endsWith("/register") == true) { + if (doc.location().endsWith("/register")) { throw AuthRequiredException("/login".inContextOf(doc)) } val scripts = doc.head().select("script") - val pg = doc.body().getElementById("pg").html() + val pg = (doc.body().getElementById("pg")?.html() ?: parseFailed("Element #pg not found")) .substringAfter('=') .substringBeforeLast(';') val pages = JSONArray(pg) @@ -196,7 +197,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : result += MangaTag( source = source, key = x.getInt("id").toString(), - title = x.getString("name").capitalize() + title = x.getString("name").toCamelCase() ) } return result diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt index 4fb4085dc..44fd5937e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -51,14 +51,15 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : ?: throw ParseException("Root not found") return root.select("li").mapNotNull { li -> val a = li.selectFirst("a.manga_cover") - val href = a.relUrl("href") + val href = a?.relUrl("href") + ?: return@mapNotNull null val views = li.select("p.view") val status = views.findOwnText { x -> x.startsWith("Status:") } - ?.substringAfter(':')?.trim()?.toLowerCase(Locale.ROOT) + ?.substringAfter(':')?.trim()?.lowercase(Locale.ROOT) Manga( id = generateUid(href), title = a.attr("title"), - coverUrl = a.selectFirst("img").absUrl("src"), + coverUrl = a.selectFirst("img")?.absUrl("src").orEmpty(), source = MangaSource.MANGATOWN, altTitle = null, rating = li.selectFirst("p.score")?.selectFirst("b") @@ -87,11 +88,11 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml() val root = doc.body().selectFirst("section.main") ?.selectFirst("div.article_content") ?: throw ParseException("Cannot find root") - val info = root.selectFirst("div.detail_info").selectFirst("ul") + val info = root.selectFirst("div.detail_info")?.selectFirst("ul") val chaptersList = root.selectFirst("div.chapter_content") ?.selectFirst("ul.chapter_list")?.select("li")?.asReversed() return manga.copy( - tags = manga.tags + info.select("li").find { x -> + tags = manga.tags + info?.select("li")?.find { x -> x.selectFirst("b")?.ownText() == "Genre(s):" }?.select("a")?.mapNotNull { a -> MangaTag( @@ -100,9 +101,10 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : source = MangaSource.MANGATOWN ) }.orEmpty(), - description = info.getElementById("show")?.ownText(), + description = info?.getElementById("show")?.ownText(), chapters = chaptersList?.mapIndexedNotNull { i, li -> - val href = li.selectFirst("a").relUrl("href") + val href = li.selectFirst("a")?.relUrl("href") + ?: return@mapIndexedNotNull null val name = li.select("span").filter { it.className().isEmpty() } .joinToString(" - ") { it.text() }.trim() MangaChapter( @@ -110,7 +112,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : url = href, source = MangaSource.MANGATOWN, number = i + 1, - name = if (name.isEmpty()) "${manga.title} - ${i + 1}" else name + name = name.ifEmpty { "${manga.title} - ${i + 1}" } ) } ) @@ -121,7 +123,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : val doc = loaderContext.httpGet(fullUrl).parseHtml() val root = doc.body().selectFirst("div.page_select") ?: throw ParseException("Cannot find root") - return root.selectFirst("select").select("option").mapNotNull { + return root.selectFirst("select")?.select("option")?.mapNotNull { val href = it.relUrl("value") if (href.endsWith("featured.html")) { return@mapNotNull null @@ -132,20 +134,20 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : referer = fullUrl, source = MangaSource.MANGATOWN ) - } + } ?: parseFailed("Pages list not found") } override suspend fun getPageUrl(page: MangaPage): String { val doc = loaderContext.httpGet(page.url.withDomain()).parseHtml() - return doc.getElementById("image").absUrl("src") + return doc.getElementById("image")?.absUrl("src") ?: parseFailed("Image not found") } override suspend fun getTags(): Set { val doc = loaderContext.httpGet("/directory/".withDomain()).parseHtml() val root = doc.body().selectFirst("aside.right") - .getElementsContainingOwnText("Genres") - .first() - .nextElementSibling() + ?.getElementsContainingOwnText("Genres") + ?.first() + ?.nextElementSibling() ?: parseFailed("Root not found") return root.select("li").mapNotNullToSet { li -> val a = li.selectFirst("a") ?: return@mapNotNullToSet null val key = a.attr("href").parseTagKey() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index 96430b755..ed390f61a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -43,25 +43,26 @@ class MangareadRepository( payload ).parseHtml() return doc.select("div.row.c-tabs-item__content").map { div -> - val href = div.selectFirst("a").relUrl("href") + val href = div.selectFirst("a")?.relUrl("href") + ?: parseFailed("Link not found") val summary = div.selectFirst(".tab-summary") Manga( id = generateUid(href), url = href, publicUrl = href.inContextOf(div), - coverUrl = div.selectFirst("img").absUrl("src"), - title = summary.selectFirst("h3").text(), + coverUrl = div.selectFirst("img")?.absUrl("src").orEmpty(), + title = summary?.selectFirst("h3")?.text().orEmpty(), rating = div.selectFirst("span.total_votes")?.ownText() ?.toFloatOrNull()?.div(5f) ?: -1f, - tags = summary.selectFirst(".mg_genres")?.select("a")?.mapToSet { a -> + tags = summary?.selectFirst(".mg_genres")?.select("a")?.mapToSet { a -> MangaTag( key = a.attr("href").removeSuffix("/").substringAfterLast('/'), title = a.text(), source = MangaSource.MANGAREAD ) }.orEmpty(), - author = summary.selectFirst(".mg_author")?.selectFirst("a")?.ownText(), - state = when (summary.selectFirst(".mg_status")?.selectFirst(".summary-content") + author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(), + state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content") ?.ownText()?.trim()) { "OnGoing" -> MangaState.ONGOING "Completed" -> MangaState.FINISHED @@ -75,9 +76,9 @@ class MangareadRepository( override suspend fun getTags(): Set { val doc = loaderContext.httpGet("https://${getDomain()}/manga/").parseHtml() val root = doc.body().selectFirst("header") - .selectFirst("ul.second-menu") + ?.selectFirst("ul.second-menu") ?: parseFailed("Root not found") return root.select("li").mapNotNullToSet { li -> - val a = li.selectFirst("a") + val a = li.selectFirst("a") ?: return@mapNotNullToSet null val href = a.attr("href").removeSuffix("/") .substringAfterLast("genres/", "") if (href.isEmpty()) { @@ -127,10 +128,12 @@ class MangareadRepository( ?.joinToString { it.html() }, chapters = doc2.select("li").asReversed().mapIndexed { i, li -> val a = li.selectFirst("a") - val href = a.relUrl("href") + val href = a?.relUrl("href").orEmpty().ifEmpty { + parseFailed("Link is missing") + } MangaChapter( id = generateUid(href), - name = a.ownText(), + name = a!!.ownText(), number = i + 1, url = href, source = MangaSource.MANGAREAD @@ -147,7 +150,7 @@ class MangareadRepository( ?: throw ParseException("Root not found") return root.select("div.page-break").map { div -> val img = div.selectFirst("img") - val url = img.relUrl("src") + val url = img?.relUrl("src") ?: parseFailed("Page image not found") MangaPage( id = generateUid(url), url = url, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index aca8baca3..cbbd59ee1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -56,22 +56,23 @@ abstract class NineMangaRepository( ?: throw ParseException("Cannot find root") val baseHost = root.baseUri().toHttpUrl().host return root.select("li").map { node -> - val href = node.selectFirst("a").absUrl("href") + val href = node.selectFirst("a")?.absUrl("href") + ?: parseFailed("Link not found") val relUrl = href.toRelativeUrl(baseHost) val dd = node.selectFirst("dd") Manga( id = generateUid(relUrl), url = relUrl, publicUrl = href, - title = dd.selectFirst("a.bookname").text().toCamelCase(), + title = dd?.selectFirst("a.bookname")?.text()?.toCamelCase().orEmpty(), altTitle = null, - coverUrl = node.selectFirst("img").absUrl("src"), + coverUrl = node.selectFirst("img")?.absUrl("src").orEmpty(), rating = Manga.NO_RATING, author = null, tags = emptySet(), state = null, source = source, - description = dd.selectFirst("p").html(), + description = dd?.selectFirst("p")?.html(), ) } } @@ -86,7 +87,7 @@ abstract class NineMangaRepository( val infoRoot = root.selectFirst("div.bookintro") ?: throw ParseException("Cannot find info") return manga.copy( - tags = infoRoot.getElementsByAttributeValue("itemprop", "genre")?.first() + tags = infoRoot.getElementsByAttributeValue("itemprop", "genre").first() ?.select("a")?.mapToSet { a -> MangaTag( title = a.text(), @@ -94,13 +95,13 @@ abstract class NineMangaRepository( source = source, ) }.orEmpty(), - author = infoRoot.getElementsByAttributeValue("itemprop", "author")?.first()?.text(), - description = infoRoot.getElementsByAttributeValue("itemprop", "description")?.first() + author = infoRoot.getElementsByAttributeValue("itemprop", "author").first()?.text(), + description = infoRoot.getElementsByAttributeValue("itemprop", "description").first() ?.html()?.substringAfter(""), chapters = root.selectFirst("div.chapterbox")?.selectFirst("ul") ?.select("li")?.asReversed()?.mapIndexed { i, li -> val a = li.selectFirst("a") - val href = a.relUrl("href") + val href = a?.relUrl("href") ?: parseFailed("Link not found") MangaChapter( id = generateUid(href), name = a.text(), @@ -138,14 +139,14 @@ abstract class NineMangaRepository( val doc = loaderContext.httpGet("https://${getDomain()}/category/", PREDEFINED_HEADERS) .parseHtml() val root = doc.body().selectFirst("ul.genreidex") - return root.select("li").mapToSet { li -> - val a = li.selectFirst("a") + return root?.select("li")?.mapToSet { li -> + val a = li.selectFirst("a") ?: parseFailed("Link not found") MangaTag( title = a.text(), key = a.attr("href").substringBetweenLast("/", "."), source = source ) - } + } ?: parseFailed("Root not found") } class English(loaderContext: MangaLoaderContext) : NineMangaRepository( diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index cc6a7d295..fbf8326ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -6,6 +6,7 @@ import java.math.BigInteger import java.net.URLEncoder import java.security.MessageDigest import java.util.* +import kotlin.contracts.contract import kotlin.math.min fun String.longHashCode(): Long { From d2609c0560a28afd39629590eac11513d1ab3277 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 15 Jul 2021 19:54:22 +0300 Subject: [PATCH 015/180] Improve remote repository tests --- app/build.gradle | 2 + .../core/parser/site/MangareadRepository.kt | 5 +- .../network/TestCookieJar.kt} | 4 +- .../core/parser/RemoteMangaRepositoryTest.kt | 126 +++++++++++++++++ .../parser}/RepositoryTestModule.kt | 6 +- .../parser/SourceSettingsStub.kt} | 4 +- .../kotatsu/parsers/RemoteRepositoryTest.kt | 132 ------------------ .../org/koitharu/kotatsu/utils/AssertX.kt | 42 ------ .../kotatsu/utils/CoroutineTestRule.kt | 32 +++++ .../koitharu/kotatsu/utils/TestResponse.kt | 32 +++++ .../org/koitharu/kotatsu/utils/TruthExt.kt | 11 ++ build.gradle | 2 +- 12 files changed, 213 insertions(+), 185 deletions(-) rename app/src/test/java/org/koitharu/kotatsu/{parsers/TemporaryCookieJar.kt => core/network/TestCookieJar.kt} (87%) create mode 100644 app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt rename app/src/test/java/org/koitharu/kotatsu/{parsers => core/parser}/RepositoryTestModule.kt (85%) rename app/src/test/java/org/koitharu/kotatsu/{parsers/SourceSettingsMock.kt => core/parser/SourceSettingsStub.kt} (67%) delete mode 100644 app/src/test/java/org/koitharu/kotatsu/parsers/RemoteRepositoryTest.kt delete mode 100644 app/src/test/java/org/koitharu/kotatsu/utils/AssertX.kt create mode 100644 app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt create mode 100644 app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt create mode 100644 app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt diff --git a/app/build.gradle b/app/build.gradle index ce37ed57b..b4f852eb3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,6 +102,8 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' testImplementation 'junit:junit:4.13.2' + testImplementation 'com.google.truth:truth:1.1.3' testImplementation 'org.json:json:20210307' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' testImplementation 'io.insert-koin:koin-test-junit4:3.1.2' } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index ed390f61a..06fe7459e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -26,11 +26,8 @@ class MangareadRepository( sortOrder: SortOrder?, tag: MangaTag? ): List { - if (offset % PAGE_SIZE != 0) { - return emptyList() - } val payload = createRequestTemplate() - payload["page"] = (offset / PAGE_SIZE).toString() + payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString() payload["vars[meta_key]"] = when (sortOrder) { SortOrder.POPULARITY -> "_wp_manga_views" SortOrder.UPDATED -> "_latest_update" diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt b/app/src/test/java/org/koitharu/kotatsu/core/network/TestCookieJar.kt similarity index 87% rename from app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt rename to app/src/test/java/org/koitharu/kotatsu/core/network/TestCookieJar.kt index 09bdf00ed..45fee5711 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/TemporaryCookieJar.kt +++ b/app/src/test/java/org/koitharu/kotatsu/core/network/TestCookieJar.kt @@ -1,10 +1,10 @@ -package org.koitharu.kotatsu.parsers +package org.koitharu.kotatsu.core.network import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -class TemporaryCookieJar : CookieJar { +class TestCookieJar : CookieJar { private val cache = HashMap() diff --git a/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt b/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt new file mode 100644 index 000000000..50977a1bd --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt @@ -0,0 +1,126 @@ +package org.koitharu.kotatsu.core.parser + +import com.google.common.truth.Truth +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.model.SortOrder +import org.koitharu.kotatsu.parsers.repositoryTestModule +import org.koitharu.kotatsu.utils.CoroutineTestRule +import org.koitharu.kotatsu.utils.TestResponse +import org.koitharu.kotatsu.utils.ext.mapToSet +import org.koitharu.kotatsu.utils.ext.medianOrNull +import org.koitharu.kotatsu.utils.isAbsoluteUrl +import org.koitharu.kotatsu.utils.isRelativeUrl + +@RunWith(Parameterized::class) +class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest { + + private val repo by inject { + parametersOf(source) + } + + @get:Rule + val koinTestRule = KoinTestRule.create { + printLogger() + modules(repositoryTestModule) + } + + @get:Rule + val coroutineTestRule = CoroutineTestRule() + + @Test + fun list() = coroutineTestRule.runBlockingTest { + val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null) + checkMangaList(list) + } + + @Test + fun search() = coroutineTestRule.runBlockingTest { + val subject = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null) + .first() + val list = repo.getList(offset = 0, query = subject.title, sortOrder = null, tag = null) + checkMangaList(list) + Truth.assertThat(list.map { it.url }).contains(subject.url) + } + + @Test + fun tags() = coroutineTestRule.runBlockingTest { + val tags = repo.getTags() + Truth.assertThat(tags).isNotEmpty() + val keys = tags.map { it.key } + Truth.assertThat(keys).containsNoDuplicates() + Truth.assertThat(keys).doesNotContain("") + val titles = tags.map { it.title } + Truth.assertThat(titles).containsNoDuplicates() + Truth.assertThat(titles).doesNotContain("") + Truth.assertThat(tags.mapToSet { it.source }).containsExactly(source) + + val list = repo.getList(offset = 0, tag = tags.last(), query = null, sortOrder = null) + checkMangaList(list) + } + + @Test + fun details() = coroutineTestRule.runBlockingTest { + val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null) + val item = list.first() + val details = repo.getDetails(item) + + Truth.assertThat(details.chapters).isNotEmpty() + Truth.assertThat(details.publicUrl).isAbsoluteUrl() + Truth.assertThat(details.description).isNotNull() + Truth.assertThat(details.title).startsWith(item.title) + Truth.assertThat(details.source).isEqualTo(source) + + Truth.assertThat(details.chapters?.map { it.id }).containsNoDuplicates() + Truth.assertThat(details.chapters?.map { it.number }).containsNoDuplicates() + Truth.assertThat(details.chapters?.map { it.name }).doesNotContain("") + Truth.assertThat(details.chapters?.mapToSet { it.source }).containsExactly(source) + } + + @Test + fun pages() = coroutineTestRule.runBlockingTest { + val list = repo.getList(20, query = null, sortOrder = SortOrder.POPULARITY, tag = null) + val chapter = + repo.getDetails(list.first()).chapters?.firstOrNull() ?: error("Chapter is null") + val pages = repo.getPages(chapter) + + Truth.assertThat(pages).isNotEmpty() + Truth.assertThat(pages.map { it.id }).containsNoDuplicates() + Truth.assertThat(pages.mapToSet { it.source }).containsExactly(source) + + val page = pages.medianOrNull() ?: error("No page") + val pageUrl = repo.getPageUrl(page) + Truth.assertThat(pageUrl).isNotEmpty() + Truth.assertThat(pageUrl).isAbsoluteUrl() + val pageResponse = TestResponse.testRequest(pageUrl) + Truth.assertThat(pageResponse.code).isIn(200..299) + Truth.assertThat(pageResponse.type).isEqualTo("image") + } + + private fun checkMangaList(list: List) { + Truth.assertThat(list).isNotEmpty() + Truth.assertThat(list.map { it.id }).containsNoDuplicates() + for (item in list) { + Truth.assertThat(item.url).isNotEmpty() + Truth.assertThat(item.url).isRelativeUrl() + Truth.assertThat(item.coverUrl).isAbsoluteUrl() + Truth.assertThat(item.title).isNotEmpty() + Truth.assertThat(item.publicUrl).isAbsoluteUrl() + } + } + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun getProviders() = (MangaSource.values().toList() - MangaSource.LOCAL).toTypedArray() + } +} \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/RepositoryTestModule.kt b/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt similarity index 85% rename from app/src/test/java/org/koitharu/kotatsu/parsers/RepositoryTestModule.kt rename to app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt index bb31ee2bc..c28ccdec8 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/RepositoryTestModule.kt +++ b/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt @@ -5,14 +5,16 @@ import okhttp3.OkHttpClient import org.koin.dsl.module import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.network.TestCookieJar import org.koitharu.kotatsu.core.network.UserAgentInterceptor import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.parser.SourceSettingsStub import org.koitharu.kotatsu.core.prefs.SourceSettings import java.util.concurrent.TimeUnit val repositoryTestModule get() = module { - single { TemporaryCookieJar() } + single { TestCookieJar() } factory { OkHttpClient.Builder() .cookieJar(get()) @@ -25,7 +27,7 @@ val repositoryTestModule single { object : MangaLoaderContext(get(), get()) { override fun getSettings(source: MangaSource): SourceSettings { - return SourceSettingsMock() + return SourceSettingsStub() } } } diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/SourceSettingsMock.kt b/app/src/test/java/org/koitharu/kotatsu/core/parser/SourceSettingsStub.kt similarity index 67% rename from app/src/test/java/org/koitharu/kotatsu/parsers/SourceSettingsMock.kt rename to app/src/test/java/org/koitharu/kotatsu/core/parser/SourceSettingsStub.kt index e5bb09bf2..2c2f27461 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/SourceSettingsMock.kt +++ b/app/src/test/java/org/koitharu/kotatsu/core/parser/SourceSettingsStub.kt @@ -1,8 +1,8 @@ -package org.koitharu.kotatsu.parsers +package org.koitharu.kotatsu.core.parser import org.koitharu.kotatsu.core.prefs.SourceSettings -class SourceSettingsMock : SourceSettings { +class SourceSettingsStub : SourceSettings { override fun getDomain(defaultValue: String) = defaultValue diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/RemoteRepositoryTest.kt b/app/src/test/java/org/koitharu/kotatsu/parsers/RemoteRepositoryTest.kt deleted file mode 100644 index 8f5020ddd..000000000 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/RemoteRepositoryTest.kt +++ /dev/null @@ -1,132 +0,0 @@ -package org.koitharu.kotatsu.parsers - -import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.koin.core.component.inject -import org.koin.core.parameter.parametersOf -import org.koin.test.KoinTest -import org.koin.test.KoinTestRule -import org.koitharu.kotatsu.core.model.MangaSource -import org.koitharu.kotatsu.core.parser.RemoteMangaRepository -import org.koitharu.kotatsu.utils.AssertX -import org.koitharu.kotatsu.utils.ext.isDistinctBy - -@RunWith(Parameterized::class) -class RemoteRepositoryTest(source: MangaSource) : KoinTest { - - private val repo by inject { - parametersOf(source) - } - - @get:Rule - val koinTestRule = KoinTestRule.create { - printLogger() - modules(repositoryTestModule) - } - - @Test - fun list() { - val list = runBlocking { repo.getList(60) } - Assert.assertFalse("List is empty", list.isEmpty()) - Assert.assertTrue("Mangas are not distinct", list.isDistinctBy { it.id }) - val item = list.random() - AssertX.assertUrlRelative("Url is not relative", item.url) - AssertX.assertUrlAbsolute("Url is not absolute", item.coverUrl) - AssertX.assertContentType("Bad cover at ${item.url}", item.coverUrl, "image/*") - AssertX.assertContentType( - "invalid public url ${item.publicUrl}", - item.publicUrl, - "text/html" - ) - Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank()) - } - - @Test - fun search() { - val list = runBlocking { repo.getList(0, query = "tail") } - Assert.assertFalse("List is empty", list.isEmpty()) - Assert.assertTrue("Mangas are not distinct", list.isDistinctBy { it.id }) - val nextList = runBlocking { repo.getList(list.size, query = "tail") } - Assert.assertNotEquals("Search pagination is broken", list, nextList) - val item = list.random() - AssertX.assertUrlRelative("Url is not relative", item.url) - AssertX.assertContentType("Bad cover at ${item.url}", item.coverUrl, "image/*") - AssertX.assertContentType( - "invalid public url ${item.publicUrl}", - item.publicUrl, - "text/html" - ) - Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank()) - } - - @Test - fun tags() { - val tags = runBlocking { repo.getTags() } - Assert.assertFalse("No tags found", tags.isEmpty()) - val tag = tags.random() - Assert.assertFalse("Tag title is blank for $tag", tag.key.isBlank()) - Assert.assertFalse("Tag title is blank for $tag", tag.title.isBlank()) - val list = runBlocking { repo.getList(0, tag = tag) } - Assert.assertFalse("List is empty", list.isEmpty()) - val item = list.random() - AssertX.assertUrlRelative("Url is not relative", item.url) - AssertX.assertContentType("Bad cover at ${item.coverUrl}", item.coverUrl, "image/*") - AssertX.assertContentType( - "invalid public url ${item.publicUrl}", - item.publicUrl, - "text/html" - ) - Assert.assertFalse("Title is blank at ${item.url}", item.title.isBlank()) - } - - @Test - fun details() { - val manga = runBlocking { repo.getList(0) }.random() - val details = runBlocking { repo.getDetails(manga) } - Assert.assertFalse("No chapters at ${details.url}", details.chapters.isNullOrEmpty()) - AssertX.assertContentType( - "invalid public url ${details.publicUrl}", - details.publicUrl, - "text/html" - ) - Assert.assertFalse( - "Description is empty at ${details.url}", - details.description.isNullOrEmpty() - ) - Assert.assertTrue( - "Chapters are not distinct", - details.chapters.orEmpty().isDistinctBy { it.id }) - val chapter = details.chapters?.randomOrNull() ?: return - AssertX.assertUrlRelative("Url is not relative", chapter.url) - Assert.assertFalse( - "Chapter name missing at ${details.url}:${chapter.number}", - chapter.name.isBlank() - ) - } - - @Test - fun pages() { - val manga = runBlocking { repo.getList(0) }.random() - val details = runBlocking { repo.getDetails(manga) } - val chapter = checkNotNull(details.chapters?.randomOrNull()) { - "No chapters at ${details.url}" - } - val pages = runBlocking { repo.getPages(chapter) } - Assert.assertFalse("Cannot find any page at ${chapter.url}", pages.isEmpty()) - Assert.assertTrue("Pages are not distinct", pages.isDistinctBy { it.id }) - val page = pages.randomOrNull() ?: return - val fullUrl = runBlocking { repo.getPageUrl(page) } - AssertX.assertContentType("Wrong page response from $fullUrl", fullUrl, "image/*") - } - - companion object { - - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun getProviders() = (MangaSource.values().toList() - MangaSource.LOCAL).toTypedArray() - } -} \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/AssertX.kt b/app/src/test/java/org/koitharu/kotatsu/utils/AssertX.kt deleted file mode 100644 index e7f9e22cb..000000000 --- a/app/src/test/java/org/koitharu/kotatsu/utils/AssertX.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.koitharu.kotatsu.utils - -import okhttp3.OkHttpClient -import okhttp3.Request -import org.junit.Assert -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import java.net.HttpURLConnection -import java.net.URI - -object AssertX : KoinComponent { - - private val okHttp by inject() - - fun assertContentType(message: String, url: String, vararg types: String) { - Assert.assertFalse("URL is empty: $message", url.isEmpty()) - val request = Request.Builder() - .url(url) - .head() - .build() - val response = okHttp.newCall(request).execute() - when (val code = response.code) { - HttpURLConnection.HTTP_OK -> { - val type = response.body!!.contentType() - Assert.assertTrue(types.any { - val x = it.split('/') - type?.type == x[0] && (x[1] == "*" || type.subtype == x[1]) - }) - } - else -> Assert.fail("Invalid response code $code at $url: $message") - } - } - - fun assertUrlRelative(message: String, url: String) { - Assert.assertFalse(message, URI(url).isAbsolute) - } - - fun assertUrlAbsolute(message: String, url: String) { - Assert.assertTrue(message, URI(url).isAbsolute) - } - -} \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt b/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt new file mode 100644 index 000000000..4558ef197 --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/utils/CoroutineTestRule.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +class CoroutineTestRule( + private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(), +) : TestWatcher() { + + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } + + fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) { + runBlocking(testDispatcher) { + block() + } + } +} \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt b/app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt new file mode 100644 index 000000000..2fcc3e18f --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/utils/TestResponse.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.utils + +import okhttp3.OkHttpClient +import okhttp3.Request +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +data class TestResponse( + val code: Int, + val type: String?, + val subtype: String?, +) { + + companion object : KoinComponent { + + private val okHttp by inject() + + fun testRequest(url: String): TestResponse { + val request = Request.Builder() + .url(url) + .head() + .build() + val response = okHttp.newCall(request).execute() + val type = response.body?.contentType() + return TestResponse( + code = response.code, + type = type?.type, + subtype = type?.subtype, + ) + } + } +} diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt b/app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt new file mode 100644 index 000000000..14f62bfc2 --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/utils/TruthExt.kt @@ -0,0 +1,11 @@ +package org.koitharu.kotatsu.utils + +import com.google.common.truth.StringSubject +import java.util.regex.Pattern + +private val PATTERN_URL_ABSOLUTE = Pattern.compile("https?://[^\\s]+", Pattern.CASE_INSENSITIVE) +private val PATTERN_URL_RELATIVE = Pattern.compile("^/[^\\s]+", Pattern.CASE_INSENSITIVE) + +fun StringSubject.isRelativeUrl() = matches(PATTERN_URL_RELATIVE) + +fun StringSubject.isAbsoluteUrl() = matches(PATTERN_URL_ABSOLUTE) \ No newline at end of file diff --git a/build.gradle b/build.gradle index cab96dae5..de81afc1e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From aaea4147a42c63fa8f5e81972ab48408d0a01292 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 17 Jul 2021 03:03:07 +0000 Subject: [PATCH 016/180] Translated using Weblate (German) Currently translated at 37.1% (82 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ --- app/src/main/res/values-de/strings.xml | 85 +++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6b3daec9..30ccc909c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,2 +1,85 @@ - \ No newline at end of file + + Entfernen + Möchtest du wirklich deinen gesamten Leseverlauf löschen\? Diese Aktion kann nicht rückgängig gemacht werden. + Design + Seiten + Automatisch + Dunkel + Hell + Filter + Genre + Sortierung + Alle + Nach Bewertung + Neuestes + Beliebt + Nach Name + Heruntergeladene + Herunterladen abgeschlossen + Manga suchen + Suchen + Teilen %s + Teilen + Speichern + Gib den Name der Kategorie ein + Hinzufügen + Neue Kategorie hinzufügen + Zu Favoriten hinzufügen + Du hast noch keine Favoriten + Lesezeichen hinzufügen + Lesen + Verlauf ist leer + Nichts gefunden + Verlauf löschen + Erneut versuchen + Schließen + Kapitel %1$d von %2$d + Entfernte Quellen + Einstellungen + Listenmodus + Raster + Der gesamte Aktualisierungsverlauf wird gelöscht und diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher\? + Keine Aktualisierungen verfügbar + Suche nach Aktualisierungen fehlgeschlagen + Nach Aktualisierungen suchen + Aktualisierungen für Manga überprüfen + Die Feed-Aktualisierung beginnt in Kürze + Aktualisieren + Aktualisierungsfeed gelöscht + Aktualisierungsfeed löschen + Aktualisierungen + Über Aktualisierungen von Manga benachrichtigen, die du liest + Benachrichtigung anzeigen, wenn eine Aktualisierung verfügbar ist + Anwendungsaktualisierung ist verfügbar + Automatisch nach Aktualisierungen suchen + Aktualisiert + … und %1$d mehr + Vorbereitung … + Suche nach Aktualisierungen … + Warte auf Netzwerk … + Hier ist es irgendwie leer … + Kategorien … + Abbrechen … + Verarbeiten … + Manga wird heruntergeladen … + Verknüpfung erstellen … + Wird geladen … + Möchtest du die Kategorie „%s“ wirklich aus deinen Favoriten entfernen\? +\nAlle enthaltenen Manga gehen dabei verloren. + Möchtest du wirklich „%s“ aus dem lokalen Speicher des Telefons löschen\? +\nDieser Vorgang kann nicht rückgängig gemacht werden. + „%s“ aus lokalem Speicher gelöscht + „%s“ aus dem Verlauf entfernt + Detaillierte Liste + Liste + Kapitel + Einzelheiten + Fehler bei der Netzwerkverbindung + Ein Fehler ist aufgetreten + Verlauf + Favoriten + Lokaler Speicher + Menü öffnen + Menü schließen + \ No newline at end of file From b51b3460c03b59c9046b349e8cf8fc0c1a65b319 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 17 Jul 2021 03:07:03 +0000 Subject: [PATCH 017/180] Translated using Weblate (Italian) Currently translated at 31.6% (70 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ --- app/src/main/res/values-it/strings.xml | 73 +++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a6b3daec9..f9fef05f7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,2 +1,73 @@ - \ No newline at end of file + + Cronologia e cache + Nessuna descrizione + File non valido. Sono supportati solo ZIP e CBZ. + Questa operazione non è supportata + Elimina + Importa + Condividi l\'immagine + Pagina salvata correttamente + Salva la pagina + Aspetta che il carico finisca + Pagine + Automatico + Scuro + Chiaro + Tema + Filtro + Genere + Ordinamento + Tutti + Per valutazione + Più recente + Aggiornato + Popolare + Per nome + Scaricati + Scaricamento finito + Elaborazione… + Scaricamento di manga… + Cerca manga + Cerca + Condividi %s + Crea una scorciatoia… + Condividi + Salva + Inserisci il nome della categoria + Aggiungi + Aggiungi una nuova categoria + Aggiungi ai preferiti + Non hai ancora preferiti + Aggiungi un segnalibro + Leggi + La cronologia è vuota + Niente di trovato + Cancella la cronologia + Riprova + Tutta la cronologia degli aggiornamenti sarà cancellata e questa azione non può essere annullata. Sei sicuro/a\? + Chiudi + Capitolo %1$d di %2$d + Caricamento… + Fonti remote + Impostazioni + Modalità elenco + Griglia + Elenco dettagliato + Vuoi davvero rimuovere la categoria «%s» dai tuoi preferiti\? +\nTutti i manga contenuti saranno persi. + Vuoi davvero eliminare «%s» dalla memoria locale del tuo telefono\? +\nQuesta operazione non può essere annullata. + «%s» eliminato dall\'archiviazione locale + «%s» rimosso dalla cronologia + Lista + Capitoli + Dettagli + Errore di connessione alla rete + Si è verificato un errore + Cronologia + Preferiti + Archiviazione locale + Apri il menù + Chiudi il menù + \ No newline at end of file From fbac8881ce79811ee22c872259bbee4394e01dc0 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 17 Jul 2021 03:06:26 +0000 Subject: [PATCH 018/180] Translated using Weblate (French) Currently translated at 100.0% (221 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ --- app/src/main/res/values-fr/strings.xml | 224 ++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a6b3daec9..702bea500 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,2 +1,224 @@ - \ No newline at end of file + + Attendez la fin du chargement + Lire la suite + Certains fabricants peuvent modifier le comportement du système, ce qui peut interrompre les tâches d\'arrière-plan. + Sauvegarde enregistrée avec succès + Description + Bienvenue + Langues + Autre + Voulez-vous vraiment supprimer toutes les requêtes de recherche récentes \? Cette action ne peut pas être annulée. + Rechercher uniquement sur %s + Masquer la barre d\'outils lors du défilement + Le mot de passe doit comporter au moins 4 caractères + Confirmer + Entrez le mot de passe qui sera demandé au démarrage de l\'application + Suivant + … et %1$d autres + Par défaut : %s + Vous devez autoriser la visualisation de ce contenu + Se connecter + Inverser + Recherche de nouveaux chapitres + L\'historique des mises à jour sera effacé et cette action ne pourra pas être annulée. Êtes-vous sûr·e \? + Effacer le flux + Recherche de nouveaux chapitres : %1$d sur %2$d + Tous les cookies ont été retirés + Effacer les cookies + Résoudre + CAPTCHA est requis + Silencieux + La configuration choisie sera mémorisée pour ce manga + Appuyez pour réessayer + Aujourd\'hui + Groupe + Il y a longtemps + Hier + À l\'instant + Vous pouvez créer une sauvegarde de votre historique et de vos favoris et la restaurer + Les données ont été restaurées, mais il y a des erreurs + Toutes les données ont été restaurées avec succès + Fichier introuvable + Préparation… + Données restaurées + Restaurer à partir d\'une sauvegarde + Créer une sauvegarde des données + Sauvegarde et restauration + Redémarrage nécessaire + Utile pour les écrans AMOLED + Thème noir foncé + Garder au départ + Ajuster à la largeur + Ajuster à la hauteur + Ajuster au centre + Mode mise à l\'échelle + Signaler un problème sur GitHub + Nouvelle catégorie + Vous pouvez configurer le mode de lecture pour chaque manga séparément + Préférer le lecteur de droite à gauche + De droite à gauche + Aucune mise à jour disponible + Échec de la recherche de mise à jour + Recherche de mises à jour… + Vérifier les mises à jour + Version %s + À propos + Les mots de passe ne correspondent pas + Répéter le mot de passe + Demander le mot de passe au démarrage de l\'appli + Protéger l\'application + Mot de passe erroné + Entrez le mot de passe + Ne pas vérifier + Vérifier les mises à jour pour les mangas + La mise à jour des flux commencera bientôt + Mettre à Jour + Faire pivoter l\'écran + Flux de mises à jour effacé + Effacer le flux des mises à jour + En attente du réseau… + Taille : %s + Nouvelle version : %s + Connexes + Résultats de la recherche + Ici, vous verrez les nouveaux chapitres du manga que vous lisez + Mises à jour + Lire plus tard + Cette catégorie est vide + Tous les favoris + Terminé + Utiliser une connexion sécurisée (HTTPS) + Autre stockage + Impossible de trouver un stockage disponible + Non disponible + Emplacement des téléchargements de mangas + Animation des pages + Mangas récents + Étagère à mangas + Vous pouvez l\'enregistrer à partir de sources en ligne ou l\'importer à partir d\'un fichier. + Vous n\'avez pas encore enregistré de manga + Vous pouvez trouver ce qu\'il faut lire dans le menu latéral. + Les mangas que vous lisez seront affichés ici + Essayez de reformuler la requête. + Vous pouvez utiliser des catégories pour organiser vos mangas préférés. Appuyez sur « + » pour créer une catégorie + C\'est un peu vide ici… + Retirer la catégorie + Voulez-vous vraiment supprimer la catégorie « %s » de vos favoris \? +\nTous les mangas qu\'elle contient seront perdus. + Renommer + Catégories… + Catégories de favoris + Vibration + Indicateur lumineux + Son de notification + Recommencer + Paramètres des notifications + Lire depuis le début + Télécharger + Avertir des mises à jour des mangas que vous lisez + Nouveaux chapitres + Activé %1$d sur %2$d + Notifications + Ce manga a %s. Voulez-vous l\'enregistrer en entier \? + Enregistrer le manga + Ouvrir dans le navigateur + Afficher une notification si une mise à jour est disponible + Une mise à jour de l\'application est disponible + Vérifier les mises à jour automatiquement + Domaine + Stockage externe + Stockage interne + Gestes uniquement + Historique des recherches effacé + Effacer l\'historique de recherche + Vider le cache des miniatures + Erreur + Annulation… + Ne plus demander + Cette opération peut consommer beaucoup de trafic réseau + Avertissement + Continuer + Boutons de volume + Appuis sur les bords + Changer de pages + Paramètres du lecteur + Voulez-vous vraiment supprimer « %s » de la mémoire locale de votre téléphone \? +\nCette opération ne peut pas être annulée. + Supprimer le manga + Rechercher sur %s + Taille de la grille + Mode lecture + Webtoon + Standard + o|ko|Mo|Go|To + Cache + Vider le cache des pages + Historique et cache + Aucune description + Fichier invalide. Seuls les fichiers ZIP et CBZ sont pris en charge. + Cette opération n\'est pas prise en charge + Supprimer + Importer + Partager l\'image + Page sauvegardée avec succès + Sauvegarder la page + « %s » supprimé du stockage local + « %s » retiré de l\'historique + Retirer + Voulez-vous vraiment effacer tout votre historique de lecture \? Cette action ne peut pas être annulée. + Effacer + Pages + Automatique + Sombre + Clair + Thème + Filtre + Genre + Ordre de tri + Tous + Par évaluation + Le plus récent + Mis à jour + Populaire + Par nom + Téléchargements + Téléchargement terminé + Traitement… + Téléchargement de mangas… + Rechercher un manga + Rechercher + Partager %s + Créer un raccourci… + Partager + Enregistrer + Entrez le nom de la catégorie + Ajouter + Ajouter une nouvelle catégorie + Ajouter aux favoris + Vous n\'avez pas encore de favoris + Ajouter un marque-page + Lire + L\'histoire est vide + Rien n\'a été trouvé + Effacer l\'historique + Réessayer + Fermer + Chapitre %1$d sur %2$d + Chargement… + Sources distantes + Paramètres + Mode liste + Grille + Liste détaillée + Liste + Chapitres + Détails + Erreur de connexion au réseau + Une erreur s\'est produite + Historique + Favoris + Stockage local + Ouvrir le menu + Fermer le menu + \ No newline at end of file From 2f89c0bb92cfeae14302c073e5d5874a913682e3 Mon Sep 17 00:00:00 2001 From: HelaBasa Date: Tue, 20 Jul 2021 20:13:36 +0200 Subject: [PATCH 019/180] Added translation using Weblate (Sinhala) --- app/src/main/res/values-si/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-si/strings.xml diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-si/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 78fe18735ba4f17619ff0a01ad8f3bb47168e352 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jul 2021 12:03:18 +0300 Subject: [PATCH 020/180] Database migrations test --- app/build.gradle | 11 ++++ .../kotatsu/core/db/MangaDatabaseTest.kt | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt diff --git a/app/build.gradle b/app/build.gradle index b4f852eb3..d8fb63356 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,6 +16,7 @@ android { versionCode 367 versionName '1.1.2' generatedDensities = [] + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" kapt { arguments { @@ -41,6 +42,9 @@ android { buildFeatures { viewBinding true } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } lintOptions { disable 'MissingTranslation' abortOnError false @@ -106,4 +110,11 @@ dependencies { testImplementation 'org.json:json:20210307' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' testImplementation 'io.insert-koin:koin-test-junit4:3.1.2' + + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test:core-ktx:1.4.0' + androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' + androidTestImplementation 'androidx.room:room-testing:2.3.0' + androidTestImplementation 'com.google.truth:truth:1.1.3' } \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt new file mode 100644 index 000000000..f0f37c2a1 --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt @@ -0,0 +1,55 @@ +package org.koitharu.kotatsu.core.db + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.koitharu.kotatsu.core.db.migrations.* +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MangaDatabaseTest { + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + MangaDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun migrateAll() { + helper.createDatabase(TEST_DB, 1).apply { + // TODO execSQL("") + close() + } + for (migration in migrations) { + helper.runMigrationsAndValidate( + TEST_DB, + migration.endVersion, + true, + migration + ) + } + } + + + private companion object { + + const val TEST_DB = "test-db" + + val migrations = arrayOf( + Migration1To2(), + Migration2To3(), + Migration3To4(), + Migration4To5(), + Migration5To6(), + Migration6To7(), + Migration7To8(), + ) + } +} \ No newline at end of file From 52e136ddef44e39c840ae991df04808f38261bad Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jul 2021 12:15:24 +0300 Subject: [PATCH 021/180] Source name in list header --- .../kotatsu/core/parser/RemoteMangaRepository.kt | 3 +++ .../kotatsu/list/ui/adapter/ListHeaderAD.kt | 14 ++++++++++++++ .../kotatsu/list/ui/adapter/MangaListAdapter.kt | 2 ++ .../koitharu/kotatsu/list/ui/model/ListHeader.kt | 5 +++++ .../kotatsu/remotelist/ui/RemoteListViewModel.kt | 6 ++++-- 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 87e7812c8..ef4c55817 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -20,6 +20,9 @@ abstract class RemoteMangaRepository( loaderContext.getSettings(source) } + val title: String + get() = source.title + override val sortOrders: Set get() = emptySet() override suspend fun getPageUrl(page: MangaPage): String = page.url.withDomain() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt new file mode 100644 index 000000000..6b7c6297d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import android.widget.TextView +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun listHeaderAD() = adapterDelegate(R.layout.item_header) { + + bind { + (itemView as TextView).text = item.text + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 0ca8ecabc..71b4c2064 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -37,6 +37,7 @@ class MangaListAdapter( .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(onRetryClick)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(onRetryClick)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) + .addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) } fun setItems(list: List, commitCallback: Runnable) { @@ -77,5 +78,6 @@ class MangaListAdapter( const val ITEM_TYPE_ERROR_STATE = 6 const val ITEM_TYPE_ERROR_FOOTER = 7 const val ITEM_TYPE_EMPTY = 8 + const val ITEM_TYPE_HEADER = 9 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt new file mode 100644 index 000000000..5119954a8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -0,0 +1,5 @@ +package org.koitharu.kotatsu.list.ui.model + +data class ListHeader( + val text: CharSequence, +) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 9df7ed507..b5c86bf2b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -10,12 +10,12 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaFilterConfig import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* class RemoteListViewModel( private val repository: MangaRepository, @@ -27,6 +27,7 @@ class RemoteListViewModel( private val listError = MutableStateFlow(null) private var appliedFilter: MangaFilter? = null private var loadingJob: Job? = null + private val headerModel = ListHeader((repository as RemoteMangaRepository).title) override val content = combine( mangaList, @@ -39,7 +40,8 @@ class RemoteListViewModel( list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string._empty)) else -> { - val result = ArrayList(list.size + 1) + val result = ArrayList(list.size + 2) + result += headerModel list.toUi(result, mode) when { error != null -> result += error.toErrorFooter() From 625b2769c6efe0f8f95abd25ff312f29a5508eed Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jul 2021 12:43:20 +0300 Subject: [PATCH 022/180] Improve search ui --- .../koitharu/kotatsu/base/ui/BaseActivity.kt | 2 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 81 +++++++++++-------- .../kotatsu/search/ui/suggestion/SearchUI.kt | 49 ----------- .../search/ui/widget/SearchEditText.kt | 32 +++++++- app/src/main/res/layout/activity_main.xml | 19 +++-- 5 files changed, 90 insertions(+), 93 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 2c4421399..d4182e8f5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -60,7 +60,7 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo toolbar?.let(this::setSupportActionBar) ViewCompat.setOnApplyWindowInsetsListener(binding.root, this) - val toolbarParams = (toolbar ?: binding.root.findViewById(R.id.toolbar_card)) + val toolbarParams = (binding.root.findViewById(R.id.toolbar_card) ?: toolbar) ?.layoutParams as? AppBarLayout.LayoutParams if (toolbarParams != null) { if (get().isToolbarHideWhenScrolling) { diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 109009f3a..25f55e51b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -6,11 +6,18 @@ import android.content.res.Configuration import android.graphics.Color import android.os.Build import android.os.Bundle -import android.view.* +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat import androidx.core.graphics.Insets -import androidx.core.view.* +import androidx.core.view.GravityCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit @@ -38,18 +45,19 @@ import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel -import org.koitharu.kotatsu.search.ui.suggestion.SearchUI import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.tracker.ui.FeedFragment import org.koitharu.kotatsu.tracker.work.TrackWorker -import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.hideKeyboard +import org.koitharu.kotatsu.utils.ext.navigationItemBackground +import org.koitharu.kotatsu.utils.ext.resolveDp class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, - View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener, - MenuItem.OnActionExpandListener { + View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener { private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) private val searchSuggestionViewModel by viewModel( @@ -58,11 +66,12 @@ class MainActivity : BaseActivity(), private lateinit var navHeaderBinding: NavigationHeaderBinding private lateinit var drawerToggle: ActionBarDrawerToggle - private var searchUi: SearchUI? = null + private var searchViewElevation = 0f override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) + searchViewElevation = binding.toolbarCard.cardElevation navHeaderBinding = NavigationHeaderBinding.inflate(layoutInflater) drawerToggle = ActionBarDrawerToggle( this, @@ -71,12 +80,17 @@ class MainActivity : BaseActivity(), R.string.open_menu, R.string.close_menu ) + drawerToggle.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_back)) + drawerToggle.setToolbarNavigationClickListener { + binding.searchView.hideKeyboard() + onBackPressed() + } binding.drawer.addDrawerListener(drawerToggle) supportActionBar?.setDisplayHomeAsUpEnabled(true) binding.searchView.apply { - setOnQueryTextFocusChangeListener(this@MainActivity) - searchUi = SearchUI.from(this, this@MainActivity) + onFocusChangeListener = this@MainActivity + searchSuggestionListener = this@MainActivity } binding.navigationView.apply { @@ -114,6 +128,12 @@ class MainActivity : BaseActivity(), viewModel.remoteSources.observe(this, this::updateSideMenu) } + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + drawerToggle.isDrawerIndicatorEnabled = + binding.drawer.getDrawerLockMode(GravityCompat.START) == DrawerLayout.LOCK_MODE_UNLOCKED + } + override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) drawerToggle.syncState() @@ -126,13 +146,14 @@ class MainActivity : BaseActivity(), override fun onBackPressed() { val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) - binding.searchView.setQuery(resources.getString(R.string._empty), false) + binding.searchView.clearFocus() when { binding.drawer.isDrawerOpen(binding.navigationView) -> binding.drawer.closeDrawer( binding.navigationView) fragment != null -> supportFragmentManager.commit { remove(fragment) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + runOnCommit { onSearchClosed() } } else -> super.onBackPressed() } @@ -205,6 +226,7 @@ class MainActivity : BaseActivity(), supportFragmentManager.commit { add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + runOnCommit { onSearchOpened() } } } } @@ -215,6 +237,7 @@ class MainActivity : BaseActivity(), } override fun onQueryClick(query: String, submit: Boolean) { + binding.searchView.query = query if (submit) { if (query.isNotEmpty()) { val source = searchSuggestionViewModel.getLocalSearchSource() @@ -225,8 +248,6 @@ class MainActivity : BaseActivity(), } searchSuggestionViewModel.saveQuery(query) } - } else { - searchUi?.query = query } } @@ -244,28 +265,6 @@ class MainActivity : BaseActivity(), }.show() } - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { - val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) - if (fragment == null) { - supportFragmentManager.commit { - add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH) - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - } - } - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { - val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) - if (fragment != null) { - supportFragmentManager.commit { - remove(fragment) - setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) - } - } - return true - } - private fun onOpenReader(manga: Manga) { val options = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { ActivityOptions.makeClipRevealAnimation( @@ -334,6 +333,20 @@ class MainActivity : BaseActivity(), binding.fab.isVisible = fragment is HistoryListFragment } + private fun onSearchOpened() { + binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + drawerToggle.isDrawerIndicatorEnabled = false + binding.toolbarCard.cardElevation = 0f + binding.appbar.elevation = searchViewElevation + } + + private fun onSearchClosed() { + binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + drawerToggle.isDrawerIndicatorEnabled = true + binding.appbar.elevation = 0f + binding.toolbarCard.cardElevation = searchViewElevation + } + private companion object { const val TAG_PRIMARY = "primary" diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt deleted file mode 100644 index 6ed23e736..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchUI.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.koitharu.kotatsu.search.ui.suggestion - -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import org.koitharu.kotatsu.R - -class SearchUI( - private val searchView: SearchView, - listener: SearchSuggestionListener, - hint: String? = null, -) { - - init { - val context = searchView.context - searchView.queryHint = hint ?: context.getString(R.string.search_manga) - searchView.setOnQueryTextListener(QueryListener(listener)) - } - - var query: String - get() = searchView.query.toString() - set(value) { - searchView.setQuery(value, false) - } - - private class QueryListener( - private val listener: SearchSuggestionListener, - ) : SearchView.OnQueryTextListener { - - override fun onQueryTextSubmit(query: String?): Boolean { - return if (!query.isNullOrBlank()) { - listener.onQueryClick(query.trim(), submit = true) - true - } else false - } - - override fun onQueryTextChange(newText: String?): Boolean { - listener.onQueryChanged(newText?.trim().orEmpty()) - return true - } - } - - companion object { - - fun from( - searchView: SearchView, - listener: SearchSuggestionListener, - ): SearchUI = SearchUI(searchView, listener) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt index 677fac24e..350b82aab 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/widget/SearchEditText.kt @@ -3,9 +3,11 @@ package org.koitharu.kotatsu.search.ui.widget import android.content.Context import android.util.AttributeSet import android.view.KeyEvent +import android.view.inputmethod.EditorInfo import androidx.annotation.AttrRes import androidx.appcompat.widget.AppCompatEditText import com.google.android.material.R +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener class SearchEditText @JvmOverloads constructor( context: Context, @@ -13,16 +15,44 @@ class SearchEditText @JvmOverloads constructor( @AttrRes defStyleAttr: Int = R.attr.editTextStyle, ) : AppCompatEditText(context, attrs, defStyleAttr) { + var searchSuggestionListener: SearchSuggestionListener? = null + + var query: String + get() = text?.trim()?.toString().orEmpty() + set(value) { + if (value != text?.toString()) { + setText(value) + setSelection(value.length) + } + } + override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { if (hasFocus()) { clearFocus() - return true + // return true } } return super.onKeyPreIme(keyCode, event) } + override fun onEditorAction(actionCode: Int) { + super.onEditorAction(actionCode) + if (actionCode == EditorInfo.IME_ACTION_SEARCH) { + searchSuggestionListener?.onQueryClick(query, submit = true) + } + } + + override fun onTextChanged( + text: CharSequence?, + start: Int, + lengthBefore: Int, + lengthAfter: Int, + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + searchSuggestionListener?.onQueryChanged(query) + } + override fun clearFocus() { super.clearFocus() text?.clear() diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 79a2eeb48..e1deeeacb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -17,6 +17,7 @@ style="@style/Widget.Kotatsu.AppBar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:stateListAnimator="@null" app:elevation="0dp"> - + android:layout_height="match_parent" + android:background="@null" + android:gravity="center_vertical" + android:hint="@string/search_manga" + android:imeOptions="actionSearch" + android:importantForAutofill="no" + android:singleLine="true" /> From ebeaf9703ffac5d17fca6da9fcf7fb0c03dc6c84 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jul 2021 18:23:27 +0300 Subject: [PATCH 023/180] Refactor download service --- .../kotatsu/core/parser/MangaRepository.kt | 10 + .../kotatsu/details/ui/DetailsFragment.kt | 3 +- .../kotatsu/download/DownloadManager.kt | 230 ++++++++++++++++++ .../kotatsu/download/DownloadNotification.kt | 211 ++++++++-------- .../kotatsu/download/DownloadService.kt | 222 +++++------------ .../kotatsu/local/ui/LocalListViewModel.kt | 11 +- .../koitharu/kotatsu/utils/LiveStateFlow.kt | 12 + 7 files changed, 424 insertions(+), 275 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/download/DownloadManager.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index cd17805d4..86bf848fd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -1,5 +1,8 @@ package org.koitharu.kotatsu.core.parser +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.qualifier.named import org.koitharu.kotatsu.core.model.* interface MangaRepository { @@ -20,4 +23,11 @@ interface MangaRepository { suspend fun getPageUrl(page: MangaPage): String suspend fun getTags(): Set + + companion object : KoinComponent { + + operator fun invoke(source: MangaSource): MangaRepository { + return get(named(source)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index b3d11d3df..e71171d51 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -23,13 +23,13 @@ import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.utils.FileSizeUtils import org.koitharu.kotatsu.utils.ext.* -import kotlin.math.roundToInt class DetailsFragment : BaseFragment(), View.OnClickListener, View.OnLongClickListener { @@ -62,6 +62,7 @@ class DetailsFragment : BaseFragment(), View.OnClickList textViewTitle.text = manga.title textViewSubtitle.textAndVisible = manga.altTitle textViewAuthor.textAndVisible = manga.author + sourceContainer.isVisible = manga.source != MangaSource.LOCAL textViewSource.text = manga.source.title textViewDescription.text = manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/DownloadManager.kt new file mode 100644 index 000000000..500542ace --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/download/DownloadManager.kt @@ -0,0 +1,230 @@ +package org.koitharu.kotatsu.download + +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import android.webkit.MimeTypeMap +import coil.ImageLoader +import coil.request.ImageRequest +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.IOException +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.local.data.MangaZip +import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.utils.CacheUtils +import org.koitharu.kotatsu.utils.ext.await +import org.koitharu.kotatsu.utils.ext.deleteAwait +import org.koitharu.kotatsu.utils.ext.waitForNetwork +import java.io.File + +class DownloadManager( + private val context: Context, + private val settings: AppSettings, + private val imageLoader: ImageLoader, + private val okHttp: OkHttpClient, + private val cache: PagesCache, + private val localMangaRepository: LocalMangaRepository, +) { + + private val connectivityManager = context.getSystemService( + Context.CONNECTIVITY_SERVICE + ) as ConnectivityManager + private val coverWidth = context.resources.getDimensionPixelSize( + androidx.core.R.dimen.compat_notification_large_icon_max_width + ) + private val coverHeight = context.resources.getDimensionPixelSize( + androidx.core.R.dimen.compat_notification_large_icon_max_height + ) + + fun downloadManga(manga: Manga, chaptersIds: Set?, startId: Int): Flow = flow { + emit(State.Preparing(startId, manga, null)) + var cover: Drawable? = null + val destination = settings.getStorageDir(context) + checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } + var output: MangaZip? = null + try { + val repo = MangaRepository(manga.source) + cover = runCatching { + imageLoader.execute( + ImageRequest.Builder(context) + .data(manga.coverUrl) + .size(coverWidth, coverHeight) + .build() + ).drawable + }.getOrNull() + emit(State.Preparing(startId, manga, cover)) + val data = if (manga.chapters == null) repo.getDetails(manga) else manga + output = MangaZip.findInDir(destination, data) + output.prepare(data) + val coverUrl = data.largeCoverUrl ?: data.coverUrl + downloadFile(coverUrl, data.publicUrl, destination).let { file -> + output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) + } + val chapters = if (chaptersIds == null) { + data.chapters.orEmpty() + } else { + data.chapters.orEmpty().filter { x -> x.id in chaptersIds } + } + for ((chapterIndex, chapter) in chapters.withIndex()) { + if (chaptersIds == null || chapter.id in chaptersIds) { + val pages = repo.getPages(chapter) + for ((pageIndex, page) in pages.withIndex()) { + failsafe@ do { + try { + val url = repo.getPageUrl(page) + val file = + cache[url] ?: downloadFile(url, page.referer, destination) + output.addPage( + chapter, + file, + pageIndex, + MimeTypeMap.getFileExtensionFromUrl(url) + ) + } catch (e: IOException) { + emit(State.WaitingForNetwork(startId, manga, cover)) + connectivityManager.waitForNetwork() + continue@failsafe + } + } while (false) + + emit(State.Progress(startId, manga, cover, + totalChapters = chapters.size, + currentChapter = chapterIndex, + totalPages = pages.size, + currentPage = pageIndex, + )) + } + } + } + emit(State.PostProcessing(startId, manga, cover)) + if (!output.compress()) { + throw RuntimeException("Cannot create target file") + } + val localManga = localMangaRepository.getFromFile(output.file) + emit(State.Done(startId, manga, cover, localManga)) + } catch (_: CancellationException) { + emit(State.Cancelling(startId, manga, cover)) + } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + emit(State.Error(startId, manga, cover, e)) + } finally { + withContext(NonCancellable) { + output?.cleanup() + File(destination, TEMP_PAGE_FILE).deleteAwait() + } + } + }.catch { e -> + emit(State.Error(startId, manga, null, e)) + } + + private suspend fun downloadFile(url: String, referer: String, destination: File): File { + val request = Request.Builder() + .url(url) + .header(CommonHeaders.REFERER, referer) + .cacheControl(CacheUtils.CONTROL_DISABLED) + .get() + .build() + val call = okHttp.newCall(request) + var attempts = MAX_DOWNLOAD_ATTEMPTS + val file = File(destination, TEMP_PAGE_FILE) + while (true) { + try { + val response = call.clone().await() + withContext(Dispatchers.IO) { + file.outputStream().use { out -> + checkNotNull(response.body).byteStream().copyTo(out) + } + } + return file + } catch (e: IOException) { + attempts-- + if (attempts <= 0) { + throw e + } else { + delay(DOWNLOAD_ERROR_DELAY) + } + } + } + } + + sealed interface State { + + val startId: Int + val manga: Manga + val cover: Drawable? + + data class Queued( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + ) : State + + data class Preparing( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + ) : State + + data class Progress( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + val totalChapters: Int, + val currentChapter: Int, + val totalPages: Int, + val currentPage: Int, + ): State + + data class WaitingForNetwork( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + ): State + + data class Done( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + val localManga: Manga, + ) : State + + data class Error( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + val error: Throwable, + ) : State + + data class Cancelling( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + ): State + + data class PostProcessing( + override val startId: Int, + override val manga: Manga, + override val cover: Drawable?, + ) : State + } + + private companion object { + + private const val MAX_DOWNLOAD_ATTEMPTS = 3 + private const val DOWNLOAD_ERROR_DELAY = 500L + private const val TEMP_PAGE_FILE = "page.tmp" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt index 10868d5e3..33b2e938b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt @@ -5,9 +5,9 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context -import android.graphics.drawable.Drawable import android.os.Build import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import org.koitharu.kotatsu.R @@ -17,137 +17,126 @@ import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.getDisplayMessage import kotlin.math.roundToInt -class DownloadNotification(private val context: Context) { +class DownloadNotification( + private val context: Context, + startId: Int, +) { private val builder = NotificationCompat.Builder(context, CHANNEL_ID) - private val manager = - context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val cancelAction = NotificationCompat.Action( + R.drawable.ic_cross, + context.getString(android.R.string.cancel), + PendingIntent.getService( + context, + startId, + DownloadService.getCancelIntent(context, startId), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + ) init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && manager.getNotificationChannel(CHANNEL_ID) == null - ) { - val channel = NotificationChannel( - CHANNEL_ID, - context.getString(R.string.downloads), - NotificationManager.IMPORTANCE_LOW - ) - channel.enableVibration(false) - channel.enableLights(false) - channel.setSound(null, null) - manager.createNotificationChannel(channel) - } builder.setOnlyAlertOnce(true) builder.setDefaults(0) builder.color = ContextCompat.getColor(context, R.color.blue_primary) } - fun fillFrom(manga: Manga) { - builder.setContentTitle(manga.title) + fun create(state: DownloadManager.State): Notification { + builder.setContentTitle(state.manga.title) builder.setContentText(context.getString(R.string.manga_downloading_)) builder.setProgress(1, 0, true) builder.setSmallIcon(android.R.drawable.stat_sys_download) - builder.setLargeIcon(null) builder.setContentIntent(null) builder.setStyle(null) - } - - fun setCancelId(startId: Int) { - if (startId == 0) { - builder.clearActions() - } else { - val intent = DownloadService.getCancelIntent(context, startId) - builder.addAction( - R.drawable.ic_cross, - context.getString(android.R.string.cancel), - PendingIntent.getService( - context, - startId, - intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) - ) + builder.setLargeIcon(state.cover?.toBitmap()) + builder.clearActions() + when (state) { + is DownloadManager.State.Cancelling -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.cancelling_)) + builder.setContentIntent(null) + builder.setStyle(null) + } + is DownloadManager.State.Done -> { + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.download_complete)) + builder.setContentIntent(createMangaIntent(context, state.localManga)) + builder.setAutoCancel(true) + builder.setSmallIcon(android.R.drawable.stat_sys_download_done) + builder.setCategory(null) + builder.setStyle(null) + } + is DownloadManager.State.Error -> { + val message = state.error.getDisplayMessage(context.resources) + builder.setProgress(0, 0, false) + builder.setSmallIcon(android.R.drawable.stat_notify_error) + builder.setSubText(context.getString(R.string.error)) + builder.setContentText(message) + builder.setAutoCancel(true) + builder.setContentIntent(null) + builder.setCategory(NotificationCompat.CATEGORY_ERROR) + builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) + } + is DownloadManager.State.PostProcessing -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.processing_)) + builder.setStyle(null) + } + is DownloadManager.State.Queued, + is DownloadManager.State.Preparing -> { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.preparing_)) + builder.setStyle(null) + builder.addAction(cancelAction) + } + is DownloadManager.State.Progress -> { + val max = state.totalChapters * PROGRESS_STEP + val progress = state.currentChapter * PROGRESS_STEP + + (state.currentPage / state.totalPages.toFloat() * PROGRESS_STEP) + .roundToInt() + val percent = (progress / max.toFloat() * 100).roundToInt() + builder.setProgress(max, progress, false) + builder.setContentText("%d%%".format(percent)) + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setStyle(null) + builder.addAction(cancelAction) + } + is DownloadManager.State.WaitingForNetwork -> { + builder.setProgress(0, 0, false) + builder.setContentText(context.getString(R.string.waiting_for_network)) + builder.setStyle(null) + builder.addAction(cancelAction) + } } + return builder.build() } - fun setError(e: Throwable) { - val message = e.getDisplayMessage(context.resources) - builder.setProgress(0, 0, false) - builder.setSmallIcon(android.R.drawable.stat_notify_error) - builder.setSubText(context.getString(R.string.error)) - builder.setContentText(message) - builder.setAutoCancel(true) - builder.setContentIntent(null) - builder.setCategory(NotificationCompat.CATEGORY_ERROR) - builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) - } - - fun setLargeIcon(icon: Drawable?) { - builder.setLargeIcon(icon?.toBitmap()) - } - - fun setProgress(chaptersTotal: Int, pagesTotal: Int, chapter: Int, page: Int) { - val max = chaptersTotal * PROGRESS_STEP - val progress = - chapter * PROGRESS_STEP + (page / pagesTotal.toFloat() * PROGRESS_STEP).roundToInt() - val percent = (progress / max.toFloat() * 100).roundToInt() - builder.setProgress(max, progress, false) - builder.setContentText("%d%%".format(percent)) - builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) - builder.setStyle(null) - } - - fun setWaitingForNetwork() { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.waiting_for_network)) - builder.setStyle(null) - } - - fun setPostProcessing() { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.processing_)) - builder.setStyle(null) - } - - fun setDone(manga: Manga) { - builder.setProgress(0, 0, false) - builder.setContentText(context.getString(R.string.download_complete)) - builder.setContentIntent(createIntent(context, manga)) - builder.setAutoCancel(true) - builder.setSmallIcon(android.R.drawable.stat_sys_download_done) - builder.setCategory(null) - builder.setStyle(null) - } - - fun setCancelling() { - builder.setProgress(1, 0, true) - builder.setContentText(context.getString(R.string.cancelling_)) - builder.setContentIntent(null) - builder.setStyle(null) - } - - fun update(id: Int = NOTIFICATION_ID) { - manager.notify(id, builder.build()) - } - - fun dismiss(id: Int = NOTIFICATION_ID) { - manager.cancel(id) - } - - operator fun invoke(): Notification = builder.build() + private fun createMangaIntent(context: Context, manga: Manga) = PendingIntent.getActivity( + context, + manga.hashCode(), + DetailsActivity.newIntent(context, manga), + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) companion object { - const val NOTIFICATION_ID = 201 - const val CHANNEL_ID = "download" - + private const val CHANNEL_ID = "download" private const val PROGRESS_STEP = 20 - private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity( - context, - manga.hashCode(), - DetailsActivity.newIntent(context, manga), - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) + fun createChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = NotificationManagerCompat.from(context) + if (manager.getNotificationChannel(CHANNEL_ID) == null) { + val channel = NotificationChannel( + CHANNEL_ID, + context.getString(R.string.downloads), + NotificationManager.IMPORTANCE_LOW + ) + channel.enableVibration(false) + channel.enableLights(false) + channel.setSound(null, null) + manager.createNotificationChannel(channel) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt index 2cc0ca30d..dd7e63dae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt @@ -3,57 +3,51 @@ package org.koitharu.kotatsu.download import android.content.Context import android.content.Intent import android.net.ConnectivityManager +import android.os.Binder +import android.os.IBinder import android.os.PowerManager -import android.webkit.MimeTypeMap import android.widget.Toast +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope -import coil.ImageLoader -import coil.request.ImageRequest import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.sync.Mutex -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.IOException +import kotlinx.coroutines.sync.withLock import org.koin.android.ext.android.get -import org.koin.android.ext.android.inject import org.koin.core.context.GlobalContext import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseService import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.core.model.Manga -import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.local.data.MangaZip -import org.koitharu.kotatsu.local.data.PagesCache -import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.utils.CacheUtils -import org.koitharu.kotatsu.utils.ext.* -import java.io.File +import org.koitharu.kotatsu.utils.LiveStateFlow +import org.koitharu.kotatsu.utils.ext.toArraySet +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.collections.set -import kotlin.math.absoluteValue class DownloadService : BaseService() { - private lateinit var notification: DownloadNotification + private lateinit var notificationManager: NotificationManagerCompat private lateinit var wakeLock: PowerManager.WakeLock - private lateinit var connectivityManager: ConnectivityManager + private lateinit var downloadManager: DownloadManager + private lateinit var dispatcher: ExecutorCoroutineDispatcher - private val okHttp by inject() - private val cache by inject() - private val settings by inject() - private val imageLoader by inject() - private val jobs = HashMap() + private val jobs = HashMap>() private val mutex = Mutex() override fun onCreate() { super.onCreate() - notification = DownloadNotification(this) - connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + notificationManager = NotificationManagerCompat.from(this) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") + downloadManager = DownloadManager(this, get(), get(), get(), get(), get()) + dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + DownloadNotification.createChannel(this) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -63,8 +57,9 @@ class DownloadService : BaseService() { val manga = intent.getParcelableExtra(EXTRA_MANGA) val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet() if (manga != null) { - jobs[startId] = downloadManga(manga, chapters, startId) + jobs[startId] = downloadManga(startId, manga, chapters) Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show() + return START_REDELIVER_INTENT } else { stopSelf(startId) } @@ -79,144 +74,59 @@ class DownloadService : BaseService() { return START_NOT_STICKY } - private fun downloadManga(manga: Manga, chaptersIds: Set?, startId: Int): Job { - return lifecycleScope.launch(Dispatchers.Default) { - mutex.lock() - wakeLock.acquire(TimeUnit.HOURS.toMillis(1)) - notification.fillFrom(manga) - notification.setCancelId(startId) - withContext(Dispatchers.Main) { - startForeground(DownloadNotification.NOTIFICATION_ID, notification()) - } - val destination = settings.getStorageDir(this@DownloadService) - checkNotNull(destination) { getString(R.string.cannot_find_available_storage) } - var output: MangaZip? = null - try { - val repo = mangaRepositoryOf(manga.source) - val cover = runCatching { - imageLoader.execute( - ImageRequest.Builder(this@DownloadService) - .data(manga.coverUrl) - .build() - ).drawable - }.getOrNull() - notification.setLargeIcon(cover) - notification.update() - val data = if (manga.chapters == null) repo.getDetails(manga) else manga - output = MangaZip.findInDir(destination, data) - output.prepare(data) - val coverUrl = data.largeCoverUrl ?: data.coverUrl - downloadFile(coverUrl, data.publicUrl, destination).let { file -> - output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) - } - val chapters = if (chaptersIds == null) { - data.chapters.orEmpty() - } else { - data.chapters.orEmpty().filter { x -> x.id in chaptersIds } - } - for ((chapterIndex, chapter) in chapters.withIndex()) { - if (chaptersIds == null || chapter.id in chaptersIds) { - val pages = repo.getPages(chapter) - for ((pageIndex, page) in pages.withIndex()) { - failsafe@ do { - try { - val url = repo.getPageUrl(page) - val file = - cache[url] ?: downloadFile(url, page.referer, destination) - output.addPage( - chapter, - file, - pageIndex, - MimeTypeMap.getFileExtensionFromUrl(url) - ) - } catch (e: IOException) { - notification.setWaitingForNetwork() - notification.update() - connectivityManager.waitForNetwork() - continue@failsafe - } - } while (false) - notification.setProgress( - chapters.size, - pages.size, - chapterIndex, - pageIndex - ) - notification.update() - } - } - } - notification.setCancelId(0) - notification.setPostProcessing() - notification.update() - if (!output.compress()) { - throw RuntimeException("Cannot create target file") - } - val result = get().getFromFile(output.file) - notification.setDone(result) - notification.dismiss() - notification.update(manga.id.toInt().absoluteValue) - } catch (_: CancellationException) { - withContext(NonCancellable) { - notification.setCancelling() - notification.setCancelId(0) - notification.update() - } - } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - notification.setError(e) - notification.setCancelId(0) - notification.dismiss() - notification.update(manga.id.toInt().absoluteValue) - } finally { - withContext(NonCancellable) { - jobs.remove(startId) - output?.cleanup() - destination.sub(TEMP_PAGE_FILE).deleteAwait() - withContext(Dispatchers.Main) { - stopForeground(true) - notification.dismiss() - stopSelf(startId) + override fun onDestroy() { + super.onDestroy() + dispatcher.close() + } + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return DownloadBinder() + } + + private fun downloadManga( + startId: Int, + manga: Manga, + chaptersIds: Set?, + ): LiveStateFlow { + val initialState = DownloadManager.State.Queued(startId, manga, null) + val stateFlow = MutableStateFlow(initialState) + val job = lifecycleScope.launch { + mutex.withLock { + wakeLock.acquire(TimeUnit.HOURS.toMillis(1)) + val notification = DownloadNotification(this@DownloadService, startId) + startForeground(startId, notification.create(initialState)) + try { + withContext(dispatcher) { + downloadManager.downloadManga(manga, chaptersIds, startId) + .collect { state -> + stateFlow.value = state + notificationManager.notify(startId, notification.create(state)) + } } + } finally { + ServiceCompat.stopForeground( + this@DownloadService, + if (isActive) { + ServiceCompat.STOP_FOREGROUND_DETACH + } else { + ServiceCompat.STOP_FOREGROUND_REMOVE + } + ) if (wakeLock.isHeld) { wakeLock.release() } - mutex.unlock() + stopSelf(startId) } } } + return LiveStateFlow(stateFlow, job) } - private suspend fun downloadFile(url: String, referer: String, destination: File): File { - val request = Request.Builder() - .url(url) - .header(CommonHeaders.REFERER, referer) - .cacheControl(CacheUtils.CONTROL_DISABLED) - .get() - .build() - val call = okHttp.newCall(request) - var attempts = MAX_DOWNLOAD_ATTEMPTS - val file = destination.sub(TEMP_PAGE_FILE) - while (true) { - try { - val response = call.clone().await() - withContext(Dispatchers.IO) { - file.outputStream().use { out -> - checkNotNull(response.body).byteStream().copyTo(out) - } - } - return file - } catch (e: IOException) { - attempts-- - if (attempts <= 0) { - throw e - } else { - delay(DOWNLOAD_ERROR_DELAY) - } - } - } + inner class DownloadBinder : Binder() { + + val downloads: Collection> + get() = jobs.values } companion object { @@ -230,10 +140,6 @@ class DownloadService : BaseService() { private const val EXTRA_CHAPTERS_IDS = "chapters_ids" private const val EXTRA_CANCEL_ID = "cancel_id" - private const val MAX_DOWNLOAD_ATTEMPTS = 3 - private const val DOWNLOAD_ERROR_DELAY = 500L - private const val TEMP_PAGE_FILE = "page.tmp" - fun start(context: Context, manga: Manga, chaptersIds: Collection? = null) { confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 2ab348b6c..c0e728c6f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -14,10 +14,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.EmptyState -import org.koitharu.kotatsu.list.ui.model.LoadingState -import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.list.ui.model.toUi +import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.SingleLiveEvent @@ -36,6 +33,7 @@ class LocalListViewModel( val onMangaRemoved = SingleLiveEvent() private val listError = MutableStateFlow(null) private val mangaList = MutableStateFlow?>(null) + private val headerModel = ListHeader(context.getString(R.string.local_storage)) override val content = combine( mangaList, @@ -46,7 +44,10 @@ class LocalListViewModel( error != null -> listOf(error.toErrorState(canRetry = true)) list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_storage, R.string.text_local_holder_primary, R.string.text_local_holder_secondary)) - else -> list.toUi(mode) + else -> ArrayList(list.size + 1).apply { + add(headerModel) + list.toUi(this, mode) + } } }.asLiveDataDistinct( viewModelScope.coroutineContext + Dispatchers.Default, diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt new file mode 100644 index 000000000..f5b1d2863 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.utils + +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow + +class LiveStateFlow( + private val stateFlow: StateFlow, + private val job: Job, +) : StateFlow by stateFlow, Job by job { + + +} \ No newline at end of file From 77186d271d43830ec9fc9479a8e9f7a348e19736 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Jul 2021 19:13:09 +0300 Subject: [PATCH 024/180] Fix list headers --- .../koitharu/kotatsu/history/ui/HistoryListViewModel.kt | 6 ++++-- .../org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt | 7 ++++++- .../java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt | 5 ++++- .../org/koitharu/kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 539d68d77..8422232bd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -20,7 +20,6 @@ import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.onFirst import java.util.* import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList class HistoryListViewModel( private val repository: HistoryRepository, @@ -81,8 +80,11 @@ class HistoryListViewModel( grouped: Boolean, mode: ListMode ): List { - val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size) + val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 1) var prevDate: DateTimeAgo? = null + if (!grouped) { + result += ListHeader(null, R.string.history) + } for ((manga, history) in list) { if (grouped) { val date = timeAgo(history.updatedAt) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt index 6b7c6297d..4d25060ac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/ListHeaderAD.kt @@ -9,6 +9,11 @@ import org.koitharu.kotatsu.list.ui.model.ListModel fun listHeaderAD() = adapterDelegate(R.layout.item_header) { bind { - (itemView as TextView).text = item.text + val textView = (itemView as TextView) + if (item.text != null) { + textView.text = item.text + } else { + textView.setText(item.textRes) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index 5119954a8..209c7227f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -1,5 +1,8 @@ package org.koitharu.kotatsu.list.ui.model +import androidx.annotation.StringRes + data class ListHeader( - val text: CharSequence, + val text: CharSequence?, + @StringRes val textRes: Int, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index c0e728c6f..42b2d570b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -33,7 +33,7 @@ class LocalListViewModel( val onMangaRemoved = SingleLiveEvent() private val listError = MutableStateFlow(null) private val mangaList = MutableStateFlow?>(null) - private val headerModel = ListHeader(context.getString(R.string.local_storage)) + private val headerModel = ListHeader(null, R.string.local_storage) override val content = combine( mangaList, diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index b5c86bf2b..b344b2bda 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -27,7 +27,7 @@ class RemoteListViewModel( private val listError = MutableStateFlow(null) private var appliedFilter: MangaFilter? = null private var loadingJob: Job? = null - private val headerModel = ListHeader((repository as RemoteMangaRepository).title) + private val headerModel = ListHeader((repository as RemoteMangaRepository).title, 0) override val content = combine( mangaList, From e8e95a485bcf947534d444977af83c897c5c1773 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 23 Jul 2021 06:51:01 +0300 Subject: [PATCH 025/180] Downloads queue activity --- app/src/main/AndroidManifest.xml | 6 +- .../kotatsu/details/ui/ChaptersFragment.kt | 2 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 +- .../download/{ => domain}/DownloadManager.kt | 16 ++- .../kotatsu/download/ui/DownloadItemAD.kt | 101 +++++++++++++++++ .../kotatsu/download/ui/DownloadsActivity.kt | 58 ++++++++++ .../kotatsu/download/ui/DownloadsAdapter.kt | 38 +++++++ .../{ => ui/service}/DownloadNotification.kt | 30 +++--- .../{ => ui/service}/DownloadService.kt | 102 ++++++++++-------- .../ui/categories/CategoriesActivity.kt | 1 + .../org/koitharu/kotatsu/local/LocalModule.kt | 2 +- .../local/domain/LocalMangaRepository.kt | 45 ++++++-- .../kotatsu/local/ui/LocalListFragment.kt | 25 ++++- .../kotatsu/local/ui/LocalListViewModel.kt | 8 +- .../kotatsu/utils/DeferredStateFlow.kt | 22 ++++ .../koitharu/kotatsu/utils/JobStateFlow.kt | 21 ++++ .../utils/LifecycleAwareServiceConnection.kt | 49 +++++++++ .../koitharu/kotatsu/utils/LiveStateFlow.kt | 12 --- .../koitharu/kotatsu/utils/ext/FragmentExt.kt | 11 +- .../org/koitharu/kotatsu/utils/ext/ViewExt.kt | 13 +++ .../main/res/layout/activity_downloads.xml | 47 ++++++++ app/src/main/res/layout/item_download.xml | 97 +++++++++++++++++ app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 24 files changed, 620 insertions(+), 92 deletions(-) rename app/src/main/java/org/koitharu/kotatsu/download/{ => domain}/DownloadManager.kt (95%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt rename app/src/main/java/org/koitharu/kotatsu/download/{ => ui/service}/DownloadNotification.kt (86%) rename app/src/main/java/org/koitharu/kotatsu/download/{ => ui/service}/DownloadService.kt (67%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/DeferredStateFlow.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/JobStateFlow.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt create mode 100644 app/src/main/res/layout/activity_downloads.xml create mode 100644 app/src/main/res/layout/item_download.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78f461918..7e221bce1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,6 +68,7 @@ android:windowSoftInputMode="stateAlwaysHidden" /> @@ -83,9 +84,12 @@ + , JobStateFlow, ItemDownloadBinding>( + { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) } +) { + + var job: Job? = null + + bind { + job?.cancel() + job = item.onEach { state -> + binding.textViewTitle.text = state.manga.title + binding.imageViewCover.setImageDrawable( + state.cover ?: getDrawable(R.drawable.ic_placeholder) + ) + when (state) { + is DownloadManager.State.Cancelling -> { + binding.textViewStatus.setText(R.string.cancelling_) + binding.progressBar.setIndeterminateCompat(true) + binding.progressBar.isVisible = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.Done -> { + binding.textViewStatus.setText(R.string.download_complete) + binding.progressBar.setIndeterminateCompat(false) + binding.progressBar.isVisible = false + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.Error -> { + binding.textViewStatus.setText(R.string.error_occurred) + binding.progressBar.setIndeterminateCompat(false) + binding.progressBar.isVisible = false + binding.textViewPercent.isVisible = false + binding.textViewDetails.text = state.error.getDisplayMessage(context.resources) + binding.textViewDetails.isVisible = true + } + is DownloadManager.State.PostProcessing -> { + binding.textViewStatus.setText(R.string.processing_) + binding.progressBar.setIndeterminateCompat(true) + binding.progressBar.isVisible = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.Preparing -> { + binding.textViewStatus.setText(R.string.preparing_) + binding.progressBar.setIndeterminateCompat(true) + binding.progressBar.isVisible = true + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.Progress -> { + binding.textViewStatus.setText(R.string.manga_downloading_) + binding.progressBar.setIndeterminateCompat(false) + binding.progressBar.isVisible = true + binding.progressBar.max = state.max + binding.progressBar.setProgressCompat(state.progress, true) + binding.textViewPercent.text = (state.percent * 100f).format(1) + "%" + binding.textViewPercent.isVisible = true + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.Queued -> { + binding.textViewStatus.setText(R.string.queued) + binding.progressBar.setIndeterminateCompat(false) + binding.progressBar.isVisible = false + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + is DownloadManager.State.WaitingForNetwork -> { + binding.textViewStatus.setText(R.string.waiting_for_network) + binding.progressBar.setIndeterminateCompat(false) + binding.progressBar.isVisible = false + binding.textViewPercent.isVisible = false + binding.textViewDetails.isVisible = false + } + } + }.launchIn(scope) + } + + onViewRecycled { + job?.cancel() + job = null + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt new file mode 100644 index 000000000..5b39881ed --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt @@ -0,0 +1,58 @@ +package org.koitharu.kotatsu.download.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.graphics.Insets +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding +import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection + +class DownloadsActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityDownloadsBinding.inflate(layoutInflater)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val adapter = DownloadsAdapter(lifecycleScope) + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter + LifecycleAwareServiceConnection.bindService( + this, + this, + Intent(this, DownloadService::class.java), + 0 + ).service.flatMapLatest { binder -> + (binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null) + }.onEach { + adapter.items = it?.toList().orEmpty() + binding.textViewHolder.isVisible = it.isNullOrEmpty() + }.launchIn(lifecycleScope) + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.recyclerView.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom + ) + binding.toolbar.updatePadding( + left = insets.left, + right = insets.right, + top = insets.top + ) + } + + companion object { + + fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt new file mode 100644 index 000000000..e6998f894 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsAdapter.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.download.ui + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import kotlinx.coroutines.CoroutineScope +import org.koitharu.kotatsu.download.domain.DownloadManager +import org.koitharu.kotatsu.utils.JobStateFlow + +class DownloadsAdapter( + scope: CoroutineScope, +) : AsyncListDifferDelegationAdapter>(DiffCallback()) { + + init { + delegatesManager.addDelegate(downloadItemAD(scope)) + setHasStableIds(true) + } + + override fun getItemId(position: Int): Long { + return items[position].value.startId.toLong() + } + + private class DiffCallback : DiffUtil.ItemCallback>() { + + override fun areItemsTheSame( + oldItem: JobStateFlow, + newItem: JobStateFlow, + ): Boolean { + return oldItem.value.startId == newItem.value.startId + } + + override fun areContentsTheSame( + oldItem: JobStateFlow, + newItem: JobStateFlow, + ): Boolean { + return oldItem.value == newItem.value + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt similarity index 86% rename from app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt rename to app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt index 33b2e938b..0d38a3326 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.download +package org.koitharu.kotatsu.download.ui.service import android.app.Notification import android.app.NotificationChannel @@ -13,9 +13,11 @@ import androidx.core.graphics.drawable.toBitmap import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.details.ui.DetailsActivity +import org.koitharu.kotatsu.download.domain.DownloadManager +import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.utils.PendingIntentCompat +import org.koitharu.kotatsu.utils.ext.format import org.koitharu.kotatsu.utils.ext.getDisplayMessage -import kotlin.math.roundToInt class DownloadNotification( private val context: Context, @@ -26,13 +28,19 @@ class DownloadNotification( private val cancelAction = NotificationCompat.Action( R.drawable.ic_cross, context.getString(android.R.string.cancel), - PendingIntent.getService( + PendingIntent.getBroadcast( context, startId, - DownloadService.getCancelIntent(context, startId), + DownloadService.getCancelIntent(startId), PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) + private val listIntent = PendingIntent.getActivity( + context, + REQUEST_LIST, + DownloadsActivity.newIntent(context), + PendingIntentCompat.FLAG_IMMUTABLE, + ) init { builder.setOnlyAlertOnce(true) @@ -45,7 +53,7 @@ class DownloadNotification( builder.setContentText(context.getString(R.string.manga_downloading_)) builder.setProgress(1, 0, true) builder.setSmallIcon(android.R.drawable.stat_sys_download) - builder.setContentIntent(null) + builder.setContentIntent(listIntent) builder.setStyle(null) builder.setLargeIcon(state.cover?.toBitmap()) builder.clearActions() @@ -72,7 +80,6 @@ class DownloadNotification( builder.setSubText(context.getString(R.string.error)) builder.setContentText(message) builder.setAutoCancel(true) - builder.setContentIntent(null) builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) } @@ -89,13 +96,8 @@ class DownloadNotification( builder.addAction(cancelAction) } is DownloadManager.State.Progress -> { - val max = state.totalChapters * PROGRESS_STEP - val progress = state.currentChapter * PROGRESS_STEP + - (state.currentPage / state.totalPages.toFloat() * PROGRESS_STEP) - .roundToInt() - val percent = (progress / max.toFloat() * 100).roundToInt() - builder.setProgress(max, progress, false) - builder.setContentText("%d%%".format(percent)) + builder.setProgress(state.max, state.progress, false) + builder.setContentText((state.percent * 100).format() + "%") builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setStyle(null) builder.addAction(cancelAction) @@ -120,7 +122,7 @@ class DownloadNotification( companion object { private const val CHANNEL_ID = "download" - private const val PROGRESS_STEP = 20 + private const val REQUEST_LIST = 6 fun createChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt similarity index 67% rename from app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt rename to app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index dd7e63dae..bda34d8e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -1,7 +1,9 @@ -package org.koitharu.kotatsu.download +package org.koitharu.kotatsu.download.ui.service +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.net.ConnectivityManager import android.os.Binder import android.os.IBinder @@ -11,11 +13,16 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.koin.android.ext.android.get import org.koin.core.context.GlobalContext import org.koitharu.kotatsu.BuildConfig @@ -24,9 +31,9 @@ import org.koitharu.kotatsu.base.ui.BaseService import org.koitharu.kotatsu.base.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.utils.LiveStateFlow +import org.koitharu.kotatsu.download.domain.DownloadManager +import org.koitharu.kotatsu.utils.JobStateFlow import org.koitharu.kotatsu.utils.ext.toArraySet -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.collections.set @@ -35,10 +42,11 @@ class DownloadService : BaseService() { private lateinit var notificationManager: NotificationManagerCompat private lateinit var wakeLock: PowerManager.WakeLock private lateinit var downloadManager: DownloadManager - private lateinit var dispatcher: ExecutorCoroutineDispatcher - private val jobs = HashMap>() + private val jobs = LinkedHashMap>() + private val jobCount = MutableStateFlow(0) private val mutex = Mutex() + private val controlReceiver = ControlReceiver() override fun onCreate() { super.onCreate() @@ -46,37 +54,23 @@ class DownloadService : BaseService() { wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") downloadManager = DownloadManager(this, get(), get(), get(), get(), get()) - dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() DownloadNotification.createChannel(this) + registerReceiver(controlReceiver, IntentFilter(ACTION_DOWNLOAD_CANCEL)) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - when (intent?.action) { - ACTION_DOWNLOAD_START -> { - val manga = intent.getParcelableExtra(EXTRA_MANGA) - val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet() - if (manga != null) { - jobs[startId] = downloadManga(startId, manga, chapters) - Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show() - return START_REDELIVER_INTENT - } else { - stopSelf(startId) - } - } - ACTION_DOWNLOAD_CANCEL -> { - val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) - jobs.remove(cancelId)?.cancel() - stopSelf(startId) - } - else -> stopSelf(startId) + val manga = intent?.getParcelableExtra(EXTRA_MANGA) + val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toArraySet() + return if (manga != null) { + jobs[startId] = downloadManga(startId, manga, chapters) + jobCount.value = jobs.size + Toast.makeText(this, R.string.manga_downloading_, Toast.LENGTH_SHORT).show() + START_REDELIVER_INTENT + } else { + stopSelf(startId) + START_NOT_STICKY } - return START_NOT_STICKY - } - - override fun onDestroy() { - super.onDestroy() - dispatcher.close() } override fun onBind(intent: Intent): IBinder { @@ -84,11 +78,16 @@ class DownloadService : BaseService() { return DownloadBinder() } + override fun onDestroy() { + unregisterReceiver(controlReceiver) + super.onDestroy() + } + private fun downloadManga( startId: Int, manga: Manga, chaptersIds: Set?, - ): LiveStateFlow { + ): JobStateFlow { val initialState = DownloadManager.State.Queued(startId, manga, null) val stateFlow = MutableStateFlow(initialState) val job = lifecycleScope.launch { @@ -97,13 +96,19 @@ class DownloadService : BaseService() { val notification = DownloadNotification(this@DownloadService, startId) startForeground(startId, notification.create(initialState)) try { - withContext(dispatcher) { + withContext(Dispatchers.Default) { downloadManager.downloadManga(manga, chaptersIds, startId) .collect { state -> stateFlow.value = state notificationManager.notify(startId, notification.create(state)) } } + if (stateFlow.value is DownloadManager.State.Done) { + sendBroadcast( + Intent(ACTION_DOWNLOAD_COMPLETE) + .putExtra(EXTRA_MANGA, manga) + ) + } } finally { ServiceCompat.stopForeground( this@DownloadService, @@ -120,19 +125,33 @@ class DownloadService : BaseService() { } } } - return LiveStateFlow(stateFlow, job) + return JobStateFlow(stateFlow, job) + } + + inner class ControlReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + when (intent?.action) { + ACTION_DOWNLOAD_CANCEL -> { + val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) + jobs.remove(cancelId)?.cancel() + jobCount.value = jobs.size + } + } + } } inner class DownloadBinder : Binder() { - val downloads: Collection> - get() = jobs.values + val downloads: Flow>> + get() = jobCount.mapLatest { jobs.values } } companion object { - private const val ACTION_DOWNLOAD_START = - "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_START" + const val ACTION_DOWNLOAD_COMPLETE = + "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE" + private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL" @@ -143,7 +162,6 @@ class DownloadService : BaseService() { fun start(context: Context, manga: Manga, chaptersIds: Collection? = null) { confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java) - intent.action = ACTION_DOWNLOAD_START intent.putExtra(EXTRA_MANGA, manga) if (chaptersIds != null) { intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) @@ -152,10 +170,8 @@ class DownloadService : BaseService() { } } - fun getCancelIntent(context: Context, startId: Int) = - Intent(context, DownloadService::class.java) - .setAction(ACTION_DOWNLOAD_CANCEL) - .putExtra(ACTION_DOWNLOAD_CANCEL, startId) + fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL) + .putExtra(ACTION_DOWNLOAD_CANCEL, startId) private fun confirmDataTransfer(context: Context, callback: () -> Unit) { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index e2ad1153e..6b4217f77 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -44,6 +44,7 @@ class CategoriesActivity : BaseActivity(), adapter = CategoriesAdapter(this) editDelegate = CategoriesEditDelegate(this, this) binding.recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) + binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = adapter binding.fabAdd.setOnClickListener(this) reorderHelper = ItemTouchHelper(ReorderHelperCallback()) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index b2a756cc1..dbe2d43e1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -15,5 +15,5 @@ val localModule single { LocalMangaRepository(androidContext()) } factory(named(MangaSource.LOCAL)) { get() } - viewModel { LocalListViewModel(get(), get(), get(), get(), androidContext()) } + viewModel { LocalListViewModel(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 2f82366af..59be0a2e5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -18,6 +18,7 @@ import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.sub +import org.koitharu.kotatsu.utils.ext.toCamelCase import java.io.File import java.util.* import java.util.zip.ZipEntry @@ -36,8 +37,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { require(offset == 0) { "LocalMangaRepository does not support pagination" } - val files = getAvailableStorageDirs(context) - .flatMap { x -> x.listFiles(filenameFilter)?.toList().orEmpty() } + val files = getAllFiles() return files.mapNotNull { x -> runCatching { getFromFile(x) }.getOrNull() } } @@ -102,7 +102,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { ) } // fallback - val title = file.nameWithoutExtension.replace("_", " ").capitalize() + val title = file.nameWithoutExtension.replace("_", " ").toCamelCase() val chapters = ArraySet() for (x in zip.entries()) { if (!x.isDirectory) { @@ -120,7 +120,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { chapters = chapters.sortedWith(AlphanumComparator()).mapIndexed { i, s -> MangaChapter( id = "$i$s".longHashCode(), - name = if (s.isEmpty()) title else s, + name = s.ifEmpty { title }, number = i + 1, source = MangaSource.LOCAL, url = uriBuilder.fragment(s).build().toString() @@ -134,13 +134,36 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { Uri.parse(localManga.url).toFile() }.getOrNull() ?: return null return withContext(Dispatchers.IO) { - val zip = ZipFile(file) - val entry = zip.getEntry(MangaZip.INDEX_ENTRY) - val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null - index.getMangaInfo() + @Suppress("BlockingMethodInNonBlockingContext") + ZipFile(file).use { zip -> + val entry = zip.getEntry(MangaZip.INDEX_ENTRY) + val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null + index.getMangaInfo() + } } } + suspend fun findSavedManga(remoteManga: Manga): Manga? = withContext(Dispatchers.IO) { + val files = getAllFiles() + for (file in files) { + @Suppress("BlockingMethodInNonBlockingContext") + val index = ZipFile(file).use { zip -> + val entry = zip.getEntry(MangaZip.INDEX_ENTRY) + entry?.let(zip::readText)?.let(::MangaIndex) + } ?: continue + val info = index.getMangaInfo() ?: continue + if (info.id == remoteManga.id) { + val fileUri = file.toUri().toString() + return@withContext info.copy( + source = MangaSource.LOCAL, + url = fileUri, + chapters = info.chapters?.map { c -> c.copy(url = fileUri) } + ) + } + } + null + } + private fun zipUri(file: File, entryName: String) = Uri.fromParts("cbz", file.path, entryName).toString() @@ -165,12 +188,16 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { override suspend fun getTags() = emptySet() + private fun getAllFiles() = getAvailableStorageDirs(context).flatMap { dir -> + dir.listFiles(filenameFilter)?.toList().orEmpty() + } + companion object { private const val DIR_NAME = "manga" fun isFileSupported(name: String): Boolean { - val ext = name.substringAfterLast('.').toLowerCase(Locale.ROOT) + val ext = name.substringAfterLast('.').lowercase(Locale.ROOT) return ext == "cbz" || ext == "zip" } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index ba700ecff..319cf5619 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -1,6 +1,6 @@ package org.koitharu.kotatsu.local.ui -import android.content.ActivityNotFoundException +import android.content.* import android.net.Uri import android.os.Bundle import android.view.Menu @@ -15,6 +15,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.utils.ext.ellipsize @@ -25,12 +26,32 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback { ActivityResultContracts.OpenDocument(), this ) + private val downloadReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == DownloadService.ACTION_DOWNLOAD_COMPLETE) { + viewModel.onRefresh() + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + context.registerReceiver( + downloadReceiver, + IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE) + ) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.onMangaRemoved.observe(viewLifecycleOwner, ::onItemRemoved) } + override fun onDetach() { + requireContext().unregisterReceiver(downloadReceiver) + super.onDetach() + } + override fun onScrolledToEnd() = Unit override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -65,7 +86,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback { override fun onActivityResult(result: Uri?) { if (result != null) { - viewModel.importFile(result) + viewModel.importFile(context?.applicationContext ?: return, result) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 42b2d570b..1991e2c0e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -19,7 +19,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import org.koitharu.kotatsu.utils.ext.sub +import java.io.File import java.io.IOException class LocalListViewModel( @@ -27,7 +27,6 @@ class LocalListViewModel( private val historyRepository: HistoryRepository, private val settings: AppSettings, private val shortcutsRepository: ShortcutsRepository, - private val context: Context ) : MangaListViewModel(settings) { val onMangaRemoved = SingleLiveEvent() @@ -71,7 +70,7 @@ class LocalListViewModel( override fun onRetry() = onRefresh() - fun importFile(uri: Uri) { + fun importFile(context: Context, uri: Uri) { launchLoadingJob { val contentResolver = context.contentResolver withContext(Dispatchers.IO) { @@ -80,8 +79,9 @@ class LocalListViewModel( if (!LocalMangaRepository.isFileSupported(name)) { throw UnsupportedFileException("Unsupported file on $uri") } - val dest = settings.getStorageDir(context)?.sub(name) + val dest = settings.getStorageDir(context)?.let { File(it, name) } ?: throw IOException("External files dir unavailable") + @Suppress("BlockingMethodInNonBlockingContext") contentResolver.openInputStream(uri)?.use { source -> dest.outputStream().use { output -> source.copyTo(output) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/DeferredStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/DeferredStateFlow.kt new file mode 100644 index 000000000..5cb69e049 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/DeferredStateFlow.kt @@ -0,0 +1,22 @@ +package org.koitharu.kotatsu.utils + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn + +class DeferredStateFlow( + private val stateFlow: StateFlow, + private val deferred: Deferred, +) : StateFlow by stateFlow, Deferred by deferred { + + suspend fun collectAndAwait(): R { + return coroutineScope { + val collectJob = launchIn(this) + val result = await() + collectJob.cancelAndJoin() + result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/JobStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/JobStateFlow.kt new file mode 100644 index 000000000..05af51f10 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/JobStateFlow.kt @@ -0,0 +1,21 @@ +package org.koitharu.kotatsu.utils + +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn + +class JobStateFlow( + private val stateFlow: StateFlow, + private val job: Job, +) : StateFlow by stateFlow, Job by job { + + suspend fun collectAndJoin(): Unit { + coroutineScope { + val collectJob = launchIn(this) + join() + collectJob.cancelAndJoin() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt new file mode 100644 index 000000000..03dd423ea --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.utils + +import android.app.Activity +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class LifecycleAwareServiceConnection private constructor( + private val host: Activity, +) : ServiceConnection, DefaultLifecycleObserver { + + private val serviceStateFlow = MutableStateFlow(null) + + val service: StateFlow + get() = serviceStateFlow + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + serviceStateFlow.value = service + } + + override fun onServiceDisconnected(name: ComponentName?) { + serviceStateFlow.value = null + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + host.unbindService(this) + } + + companion object { + + fun bindService( + host: Activity, + lifecycleOwner: LifecycleOwner, + service: Intent, + flags: Int, + ): LifecycleAwareServiceConnection { + val connection = LifecycleAwareServiceConnection(host) + host.bindService(service, connection, flags) + lifecycleOwner.lifecycle.addObserver(connection) + return connection + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt deleted file mode 100644 index f5b1d2863..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/LiveStateFlow.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.koitharu.kotatsu.utils - -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.StateFlow - -class LiveStateFlow( - private val stateFlow: StateFlow, - private val job: Job, -) : StateFlow by stateFlow, Job by job { - - -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt index 92dab70d6..069d685ff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt @@ -1,9 +1,12 @@ package org.koitharu.kotatsu.utils.ext +import android.content.Intent import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.coroutineScope +import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection inline fun T.withArgs(size: Int, block: Bundle.() -> Unit): T { val b = Bundle(size) @@ -27,4 +30,10 @@ inline fun Fragment.parcelableArgument(name: String): Lazy { @Suppress("NOTHING_TO_INLINE") inline fun Fragment.stringArgument(name: String) = lazy(LazyThreadSafetyMode.NONE) { arguments?.getString(name) -} \ No newline at end of file +} + +fun Fragment.bindService( + lifecycleOwner: LifecycleOwner, + service: Intent, + flags: Int, +) = LifecycleAwareServiceConnection.bindService(requireActivity(), lifecycleOwner, service, flags) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index c73cff93f..ac6950379 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -16,6 +16,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.progressindicator.BaseProgressIndicator import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder fun View.hideKeyboard() { @@ -158,4 +159,16 @@ fun RecyclerView.findCenterViewPosition(): Int { inline fun RecyclerView.ViewHolder.getItem(): T? { return ((this as? AdapterDelegateViewBindingViewHolder<*, *>)?.item as? T) +} + +fun BaseProgressIndicator<*>.setIndeterminateCompat(indeterminate: Boolean) { + if (isIndeterminate != indeterminate) { + if (indeterminate && visibility == View.VISIBLE) { + visibility = View.INVISIBLE + isIndeterminate = indeterminate + visibility = View.VISIBLE + } else { + isIndeterminate = indeterminate + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_downloads.xml b/app/src/main/res/layout/activity_downloads.xml new file mode 100644 index 000000000..65096b931 --- /dev/null +++ b/app/src/main/res/layout/activity_downloads.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml new file mode 100644 index 000000000..b70971dc2 --- /dev/null +++ b/app/src/main/res/layout/item_download.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 227d005eb..70c4cd7ab 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -217,4 +217,6 @@ Резервная копия успешно сохранена Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. Подробнее + В очереди + На данный момент нет активных загрузок \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 615dd5eb8..1eabb0ef4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,4 +220,6 @@ Backup saved successfully Some manufacturers can change the system behavior, which may breaks background tasks. Read more + Queued + There are currently no active downloads \ No newline at end of file From 7f5ef227ebe21bf8a4742c180151694f871e6182 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 24 Jul 2021 10:51:26 +0300 Subject: [PATCH 026/180] Show not downloaded chapters in local manga --- .../kotatsu/details/ui/ChaptersFragment.kt | 32 +++-- .../kotatsu/details/ui/DetailsActivity.kt | 30 +++++ .../kotatsu/details/ui/DetailsFragment.kt | 27 ++-- .../kotatsu/details/ui/DetailsViewModel.kt | 125 +++++++++++++++--- .../details/ui/adapter/ChapterListItemAD.kt | 9 +- .../details/ui/adapter/ChaptersAdapter.kt | 5 +- .../details/ui/model/ChapterListItem.kt | 3 +- .../ui/model/ListModelConversionExt.kt | 8 +- .../download/ui/service/DownloadService.kt | 11 +- .../local/domain/LocalMangaRepository.kt | 5 +- .../kotatsu/reader/ui/ChaptersDialog.kt | 12 +- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 6 + .../org/koitharu/kotatsu/utils/ext/FlowExt.kt | 7 + .../layout-w600dp-land/fragment_details.xml | 4 +- .../layout-w600dp-port/fragment_details.xml | 4 +- app/src/main/res/layout/fragment_details.xml | 2 + app/src/main/res/values/strings.xml | 2 + 17 files changed, 234 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index 0cb2c87d7..c2205bf30 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -15,7 +15,6 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter @@ -27,7 +26,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState class ChaptersFragment : BaseFragment(), - OnListItemClickListener, ActionMode.Callback, AdapterView.OnItemSelectedListener { + OnListItemClickListener, + ActionMode.Callback, + AdapterView.OnItemSelectedListener { private val viewModel by sharedViewModel() @@ -105,9 +106,9 @@ class ChaptersFragment : BaseFragment(), else -> super.onOptionsItemSelected(item) } - override fun onItemClick(item: MangaChapter, view: View) { + override fun onItemClick(item: ChapterListItem, view: View) { if (selectionDecoration?.checkedItemsCount != 0) { - selectionDecoration?.toggleItemChecked(item.id) + selectionDecoration?.toggleItemChecked(item.chapter.id) if (selectionDecoration?.checkedItemsCount == 0) { actionMode?.finish() } else { @@ -116,6 +117,10 @@ class ChaptersFragment : BaseFragment(), } return } + if (item.isMissing) { + (activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id) + return + } val options = ActivityOptions.makeScaleUpAnimation( view, 0, @@ -127,17 +132,17 @@ class ChaptersFragment : BaseFragment(), ReaderActivity.newIntent( view.context, viewModel.manga.value ?: return, - ReaderState(item.id, 0, 0) + ReaderState(item.chapter.id, 0, 0) ), options.toBundle() ) } - override fun onItemLongClick(item: MangaChapter, view: View): Boolean { + override fun onItemLongClick(item: ChapterListItem, view: View): Boolean { if (actionMode == null) { actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) } return actionMode?.also { - selectionDecoration?.setItemIsChecked(item.id, true) + selectionDecoration?.setItemIsChecked(item.chapter.id, true) binding.recyclerViewChapters.invalidateItemDecorations() it.invalidate() } != null @@ -148,7 +153,7 @@ class ChaptersFragment : BaseFragment(), R.id.action_save -> { DownloadService.start( context ?: return false, - viewModel.manga.value ?: return false, + viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false, selectionDecoration?.checkedItemsIds ) mode.finish() @@ -174,17 +179,20 @@ class ChaptersFragment : BaseFragment(), override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { val manga = viewModel.manga.value mode.menuInflater.inflate(R.menu.mode_chapters, menu) - menu.findItem(R.id.action_save).isVisible = manga?.source != MangaSource.LOCAL mode.title = manga?.title return true } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = selectionDecoration?.checkedItemsCount ?: return false + val selectedIds = selectionDecoration?.checkedItemsIds ?: return false + val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty() + menu.findItem(R.id.action_save).isVisible = items.none { x -> + x.chapter.source == MangaSource.LOCAL + } mode.subtitle = resources.getQuantityString( R.plurals.chapters_from_x, - count, - count, + items.size, + items.size, chaptersAdapter?.itemCount ?: 0 ) return true diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 4a9f6e1ba..84745b56d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -34,8 +34,11 @@ import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.buildAlertDialog import org.koitharu.kotatsu.utils.ext.getDisplayMessage class DetailsActivity : BaseActivity(), @@ -228,6 +231,33 @@ class DetailsActivity : BaseActivity(), binding.pager.isUserInputEnabled = true } + fun showChapterMissingDialog(chapterId: Long) { + val remoteManga = viewModel.getRemoteManga() + if (remoteManga == null) { + Snackbar.make(binding.pager, R.string.chapter_is_missing, Snackbar.LENGTH_LONG) + .show() + return + } + buildAlertDialog(this) { + setMessage(R.string.chapter_is_missing_text) + setTitle(R.string.chapter_is_missing) + setNegativeButton(android.R.string.cancel, null) + setPositiveButton(R.string.read) { _, _ -> + startActivity( + ReaderActivity.newIntent( + this@DetailsActivity, + remoteManga, + ReaderState(chapterId, 0, 0) + ) + ) + } + setNeutralButton(R.string.download) { _, _ -> + DownloadService.start(this@DetailsActivity, remoteManga, setOf(chapterId)) + } + setCancelable(true) + }.show() + } + companion object { const val ACTION_MANGA_VIEW = "${BuildConfig.APPLICATION_ID}.action.VIEW_MANGA" diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index e71171d51..05ed0a9c4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -128,23 +128,32 @@ class DetailsFragment : BaseFragment(), View.OnClickList } private fun onLoadingStateChanged(isLoading: Boolean) { - binding.progressBar.isVisible = isLoading + if (isLoading) { + binding.progressBar.show() + } else { + binding.progressBar.hide() + } } override fun onClick(v: View) { - val manga = viewModel.manga.value + val manga = viewModel.manga.value ?: return when (v.id) { R.id.button_favorite -> { - FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return) + FavouriteCategoriesDialog.show(childFragmentManager, manga) } R.id.button_read -> { - startActivity( - ReaderActivity.newIntent( - context ?: return, - manga ?: return, - null + val chapterId = viewModel.readingHistory.value?.chapterId + if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) { + (activity as? DetailsActivity)?.showChapterMissingDialog(chapterId) + } else { + startActivity( + ReaderActivity.newIntent( + context ?: return, + manga, + null + ) ) - ) + } } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 4c0292ad5..ebd377a50 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -11,7 +11,11 @@ import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaChapter +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.ChapterExtra @@ -29,7 +33,7 @@ class DetailsViewModel( private val localMangaRepository: LocalMangaRepository, private val trackingRepository: TrackingRepository, private val mangaDataRepository: MangaDataRepository, - private val settings: AppSettings + private val settings: AppSettings, ) : BaseViewModel() { private val mangaData = MutableStateFlow(intent.manga) @@ -53,6 +57,18 @@ class DetailsViewModel( trackingRepository.getNewChaptersCount(mangaId) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) + private val remoteManga = MutableStateFlow(null) + /*private val remoteManga = mangaData.mapLatest { + if (it?.source == MangaSource.LOCAL) { + runCatching { + val m = localMangaRepository.getRemoteManga(it) ?: return@mapLatest null + MangaRepository(m.source).getDetails(m) + }.getOrNull() + } else { + null + } + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)*/ + private val chaptersReversed = settings.observe() .filter { it == AppSettings.KEY_REVERSE_CHAPTERS } .map { settings.chaptersReverse } @@ -85,24 +101,19 @@ class DetailsViewModel( val chapters = combine( mangaData.map { it?.chapters.orEmpty() }, + remoteManga, history.map { it?.chapterId }, newChapters, - chaptersReversed, selectedBranch - ) { chapters, currentId, newCount, reversed, branch -> - val currentIndex = chapters.indexOfFirst { it.id == currentId } - val firstNewIndex = chapters.size - newCount - val res = chapters.mapIndexed { index, chapter -> - chapter.toListItem( - when { - index >= firstNewIndex -> ChapterExtra.NEW - index == currentIndex -> ChapterExtra.CURRENT - index < currentIndex -> ChapterExtra.READ - else -> ChapterExtra.UNREAD - } - ) - }.filter { it.chapter.branch == branch } - if (reversed) res.asReversed() else res + ) { chapters, sourceManga, currentId, newCount, branch -> + val sourceChapters = sourceManga?.chapters + if (sourceChapters.isNullOrEmpty()) { + mapChapters(chapters, currentId, newCount, branch) + } else { + mapChaptersWithSource(chapters, sourceChapters, currentId, newCount, branch) + } + }.combine(chaptersReversed) { list, reversed -> + if (reversed) list.asReversed() else list }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) init { @@ -121,6 +132,12 @@ class DetailsViewModel( ?.maxByOrNull { it.value.size }?.key } mangaData.value = manga + if (manga.source == MangaSource.LOCAL) { + remoteManga.value = runCatching { + val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null + MangaRepository(m.source).getDetails(m) + }.getOrNull() + } } } @@ -142,4 +159,80 @@ class DetailsViewModel( fun setSelectedBranch(branch: String?) { selectedBranch.value = branch } + + fun getRemoteManga(): Manga? { + return remoteManga.value + } + + private fun mapChapters( + chapters: List, + currentId: Long?, + newCount: Int, + branch: String?, + ): List { + val result = ArrayList(chapters.size) + val currentIndex = chapters.indexOfFirst { it.id == currentId } + val firstNewIndex = chapters.size - newCount + for (i in chapters.indices) { + val chapter = chapters[i] + if (chapter.branch != branch) { + continue + } + result += chapter.toListItem( + extra = when { + i >= firstNewIndex -> ChapterExtra.NEW + i == currentIndex -> ChapterExtra.CURRENT + i < currentIndex -> ChapterExtra.READ + else -> ChapterExtra.UNREAD + }, + isMissing = false + ) + } + return result + } + + private fun mapChaptersWithSource( + chapters: List, + sourceChapters: List, + currentId: Long?, + newCount: Int, + branch: String?, + ): List { + val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id } + val result = ArrayList(sourceChapters.size) + val currentIndex = sourceChapters.indexOfFirst { it.id == currentId } + val firstNewIndex = sourceChapters.size - newCount + for (i in sourceChapters.indices) { + val chapter = sourceChapters[i] + if (chapter.branch != branch) { + continue + } + val localChapter = chaptersMap.remove(chapter.id) + result += localChapter?.toListItem( + extra = when { + i >= firstNewIndex -> ChapterExtra.NEW + i == currentIndex -> ChapterExtra.CURRENT + i < currentIndex -> ChapterExtra.READ + else -> ChapterExtra.UNREAD + }, + isMissing = false + ) ?: chapter.toListItem( + extra = when { + i >= firstNewIndex -> ChapterExtra.NEW + i == currentIndex -> ChapterExtra.CURRENT + i < currentIndex -> ChapterExtra.READ + else -> ChapterExtra.UNREAD + }, + isMissing = true + ) + } + if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source + result.ensureCapacity(result.size + chaptersMap.size) + chaptersMap.values.mapTo(result) { + it.toListItem(ChapterExtra.UNREAD, false) + } + result.sortBy { it.chapter.number } + } + return result + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt index d339216c0..983f322a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChapterListItemAD.kt @@ -3,23 +3,22 @@ package org.koitharu.kotatsu.details.ui.adapter import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.databinding.ItemChapterBinding import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.history.domain.ChapterExtra import org.koitharu.kotatsu.utils.ext.getThemeColor fun chapterListItemAD( - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( { inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) } ) { itemView.setOnClickListener { - clickListener.onItemClick(item.chapter, it) + clickListener.onItemClick(item, it) } itemView.setOnLongClickListener { - clickListener.onItemLongClick(item.chapter, it) + clickListener.onItemLongClick(item, it) } bind { payload -> @@ -43,5 +42,7 @@ fun chapterListItemAD( binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse)) } } + binding.textViewTitle.alpha = if (item.isMissing) 0.3f else 1f + binding.textViewNumber.alpha = if (item.isMissing) 0.3f else 1f } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt index b98a427e7..46dc930d1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt @@ -3,12 +3,11 @@ package org.koitharu.kotatsu.details.ui.adapter import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.details.ui.model.ChapterListItem import kotlin.jvm.internal.Intrinsics class ChaptersAdapter( - onItemClickListener: OnListItemClickListener + onItemClickListener: OnListItemClickListener, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -38,7 +37,7 @@ class ChaptersAdapter( } override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? { - if (oldItem.extra != newItem.extra) { + if (oldItem.extra != newItem.extra && oldItem.chapter == newItem.chapter) { return newItem.extra } return null diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt index 5fad6e03a..82f00decf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt @@ -5,5 +5,6 @@ import org.koitharu.kotatsu.history.domain.ChapterExtra data class ChapterListItem( val chapter: MangaChapter, - val extra: ChapterExtra + val extra: ChapterExtra, + val isMissing: Boolean, ) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt index dc1df8e0f..0a1609989 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/ListModelConversionExt.kt @@ -3,7 +3,11 @@ package org.koitharu.kotatsu.details.ui.model import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.history.domain.ChapterExtra -fun MangaChapter.toListItem(extra: ChapterExtra) = ChapterListItem( +fun MangaChapter.toListItem( + extra: ChapterExtra, + isMissing: Boolean, +) = ChapterListItem( chapter = this, - extra = extra + extra = extra, + isMissing = isMissing, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index bda34d8e2..d44204533 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -47,6 +47,7 @@ class DownloadService : BaseService() { private val jobCount = MutableStateFlow(0) private val mutex = Mutex() private val controlReceiver = ControlReceiver() + private var binder: DownloadBinder? = null override fun onCreate() { super.onCreate() @@ -75,11 +76,12 @@ class DownloadService : BaseService() { override fun onBind(intent: Intent): IBinder { super.onBind(intent) - return DownloadBinder() + return binder ?: DownloadBinder(this).also { binder = it } } override fun onDestroy() { unregisterReceiver(controlReceiver) + binder = null super.onDestroy() } @@ -141,10 +143,10 @@ class DownloadService : BaseService() { } } - inner class DownloadBinder : Binder() { + class DownloadBinder(private val service: DownloadService) : Binder() { val downloads: Flow>> - get() = jobCount.mapLatest { jobs.values } + get() = service.jobCount.mapLatest { service.jobs.values } } companion object { @@ -160,6 +162,9 @@ class DownloadService : BaseService() { private const val EXTRA_CANCEL_ID = "cancel_id" fun start(context: Context, manga: Manga, chaptersIds: Collection? = null) { + if (chaptersIds?.isEmpty() == true) { + return + } confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java) intent.putExtra(EXTRA_MANGA, manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 59be0a2e5..baaab7400 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -98,7 +98,10 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { entryName = index.getCoverEntry() ?: findFirstEntry(zip.entries(), isImage = true)?.name.orEmpty() ), - chapters = info.chapters?.map { c -> c.copy(url = fileUri) } + chapters = info.chapters?.map { c -> + c.copy(url = fileUri, + source = MangaSource.LOCAL) + } ) } // fallback diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt index 667d7113a..78d713fff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ChaptersDialog.kt @@ -15,16 +15,17 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.databinding.DialogChaptersBinding import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter +import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.history.domain.ChapterExtra import org.koitharu.kotatsu.utils.ext.withArgs class ChaptersDialog : AlertDialogFragment(), - OnListItemClickListener { + OnListItemClickListener { override fun onInflateView( inflater: LayoutInflater, - container: ViewGroup? + container: ViewGroup?, ) = DialogChaptersBinding.inflate(inflater, container, false) override fun onBuildDialog(builder: AlertDialog.Builder) { @@ -51,7 +52,8 @@ class ChaptersDialog : AlertDialogFragment(), index < currentPosition -> ChapterExtra.READ index == currentPosition -> ChapterExtra.CURRENT else -> ChapterExtra.UNREAD - } + }, + isMissing = false ) }) { if (currentPosition >= 0) { @@ -66,11 +68,11 @@ class ChaptersDialog : AlertDialogFragment(), } } - override fun onItemClick(item: MangaChapter, view: View) { + override fun onItemClick(item: ChapterListItem, view: View) { ((parentFragment as? OnChapterChangeListener) ?: (activity as? OnChapterChangeListener))?.let { dismiss() - it.onChapterChanged(item) + it.onChapterChanged(item.chapter) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index b9cc851c4..87278b1d7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.utils.ext +import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkRequest +import androidx.appcompat.app.AlertDialog import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -19,4 +21,8 @@ suspend fun ConnectivityManager.waitForNetwork(): Network { unregisterNetworkCallback(callback) } } +} + +inline fun buildAlertDialog(context: Context, block: AlertDialog.Builder.() -> Unit): AlertDialog { + return AlertDialog.Builder(context).apply(block).create() } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt index a3de68094..ef2f7a422 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform fun Flow.onFirst(action: suspend (T) -> Unit): Flow { var isFirstCall = true @@ -16,4 +17,10 @@ fun Flow.onFirst(action: suspend (T) -> Unit): Flow { inline fun Flow>.mapItems(crossinline transform: (T) -> R): Flow> { return map { list -> list.map(transform) } +} + +inline fun Flow.filterNotNull( + crossinline predicate: suspend (T) -> Boolean, +): Flow = transform { value -> + if (value != null && predicate(value)) return@transform emit(value) } \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp-land/fragment_details.xml b/app/src/main/res/layout-w600dp-land/fragment_details.xml index 51f3213ed..30484f1a8 100644 --- a/app/src/main/res/layout-w600dp-land/fragment_details.xml +++ b/app/src/main/res/layout-w600dp-land/fragment_details.xml @@ -263,13 +263,15 @@ - - Read more Queued There are currently no active downloads + This chapter is missing on your device. Download it or read online + Chapter is missing \ No newline at end of file From 6f7efa9e26e744fbfc51991a88996cf9651e7949 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 28 Jul 2021 08:03:16 +0300 Subject: [PATCH 027/180] Optimize layout --- .../kotatsu/base/ui/widgets/ChipsView.kt | 8 +- .../kotatsu/details/ui/DetailsFragment.kt | 14 +-- .../koitharu/kotatsu/main/ui/MainActivity.kt | 26 ++-- app/src/main/res/layout/item_empty_state.xml | 1 + app/src/main/res/layout/item_error_footer.xml | 2 +- .../main/res/layout/item_loading_footer.xml | 9 +- app/src/main/res/layout/item_manga_grid.xml | 63 ++++------ .../res/layout/item_manga_list_details.xml | 116 +++++++++--------- app/src/main/res/layout/navigation_header.xml | 53 ++++---- app/src/main/res/values/dimens.xml | 3 +- 10 files changed, 142 insertions(+), 153 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt index e766dbf08..f8a839efc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt @@ -37,15 +37,15 @@ class ChipsView @JvmOverloads constructor( } } - fun setChips(items: List) { + fun setChips(items: Collection) { suppressLayoutCompat(true) try { for ((i, model) in items.withIndex()) { val chip = getChildAt(i) as Chip? ?: addChip() bindChip(chip, model) } - for (i in items.size until childCount) { - removeViewAt(i) + if (childCount > items.size) { + removeViews(items.size, childCount - items.size) } } finally { suppressLayoutCompat(false) @@ -60,6 +60,7 @@ class ChipsView @JvmOverloads constructor( chip.isCheckedIconVisible = true chip.setChipIconResource(model.icon) } + chip.isClickable = onChipClickListener != null chip.tag = model.data } @@ -71,7 +72,6 @@ class ChipsView @JvmOverloads constructor( chip.isCloseIconVisible = false chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) - chip.isClickable = onChipClickListener != null addView(chip) return chip } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 05ed0a9c4..a427f2b3e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -13,7 +13,6 @@ import androidx.core.view.updatePadding import coil.ImageLoader import coil.util.CoilUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject @@ -30,13 +29,13 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.utils.FileSizeUtils import org.koitharu.kotatsu.utils.ext.* +import kotlin.random.Random class DetailsFragment : BaseFragment(), View.OnClickListener, View.OnLongClickListener { private val viewModel by sharedViewModel() private val coil by inject(mode = LazyThreadSafetyMode.NONE) - private var tagsJob: Job? = null override fun onInflateView( inflater: LayoutInflater, @@ -196,16 +195,13 @@ class DetailsFragment : BaseFragment(), View.OnClickList } private fun bindTags(manga: Manga) { - tagsJob?.cancel() - tagsJob = viewLifecycleScope.launch { - val tags = ArrayList(manga.tags.size + 2) - for (tag in manga.tags) { - tags += ChipsView.ChipModel( + binding.chipsTags.setChips( + manga.tags.map { tag -> + ChipsView.ChipModel( title = tag.title, icon = 0 ) } - binding.chipsTags.setChips(tags) - } + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 25f55e51b..2788a6ee0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -13,10 +13,7 @@ import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.graphics.Insets -import androidx.core.view.GravityCompat -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.core.view.updatePadding +import androidx.core.view.* import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction @@ -88,17 +85,18 @@ class MainActivity : BaseActivity(), binding.drawer.addDrawerListener(drawerToggle) supportActionBar?.setDisplayHomeAsUpEnabled(true) - binding.searchView.apply { + with(binding.searchView) { onFocusChangeListener = this@MainActivity searchSuggestionListener = this@MainActivity } - binding.navigationView.apply { + with(binding.navigationView) { val menuView = findViewById(com.google.android.material.R.id.design_navigation_view) - navHeaderBinding.root.setOnApplyWindowInsetsListener { v, insets -> - v.updatePadding(top = insets.systemWindowInsetTop) + ViewCompat.setOnApplyWindowInsetsListener(navHeaderBinding.root) { v, insets -> + val systemWindowInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding(top = systemWindowInsets.top) // NavigationView doesn't dispatch insets to the menu view, so pad the bottom here. - menuView.updatePadding(bottom = insets.systemWindowInsetBottom) + menuView.updatePadding(bottom = systemWindowInsets.bottom) insets } addHeaderView(navHeaderBinding.root) @@ -117,9 +115,7 @@ class MainActivity : BaseActivity(), openDefaultSection() } if (savedInstanceState == null) { - TrackWorker.setup(applicationContext) - AppUpdateChecker(this).launchIfNeeded() - OnboardDialogFragment.showWelcome(get(), supportFragmentManager) + onFirstStart() } viewModel.onOpenReader.observe(this, this::onOpenReader) @@ -347,6 +343,12 @@ class MainActivity : BaseActivity(), binding.toolbarCard.cardElevation = searchViewElevation } + private fun onFirstStart() { + TrackWorker.setup(applicationContext) + AppUpdateChecker(this@MainActivity).launchIfNeeded() + OnboardDialogFragment.showWelcome(get(), supportFragmentManager) + } + private companion object { const val TAG_PRIMARY = "primary" diff --git a/app/src/main/res/layout/item_empty_state.xml b/app/src/main/res/layout/item_empty_state.xml index 83385bb3f..0d7f40c65 100644 --- a/app/src/main/res/layout/item_empty_state.xml +++ b/app/src/main/res/layout/item_empty_state.xml @@ -13,6 +13,7 @@ android:layout_width="98dp" android:layout_height="98dp" android:layout_marginBottom="16dp" + tools:ignore="ContentDescription" tools:src="@drawable/ic_alert_outline" /> + android:layout_height="@dimen/list_footer_height_outer"> + android:indeterminate="true" + app:indicatorSize="@dimen/list_footer_height_inner" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index 6a168ea68..439dd081e 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -1,50 +1,41 @@ - + android:background="@drawable/list_selector" + android:orientation="vertical"> - + android:layout_height="wrap_content" + android:layout_margin="4dp" + app:cardCornerRadius="4dp" + app:cardElevation="4dp"> - + android:scaleType="centerCrop" + android:orientation="horizontal" + tools:ignore="ContentDescription" /> - + - - - - - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_manga_list_details.xml b/app/src/main/res/layout/item_manga_list_details.xml index 11ebedae3..2c8e07b0e 100644 --- a/app/src/main/res/layout/item_manga_list_details.xml +++ b/app/src/main/res/layout/item_manga_list_details.xml @@ -8,86 +8,80 @@ android:background="@drawable/list_selector" android:orientation="horizontal"> + + + + + + + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="8dp" + android:orientation="vertical"> - - - + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.Kotatsu.ToolbarTitle" + android:textSize="18sp" + tools:text="@tools:sample/lorem/random" /> - + + android:orientation="horizontal"> - - - - - - - - - + android:layout_gravity="bottom" + android:drawablePadding="4dp" + android:paddingStart="6dp" + app:drawableEndCompat="@drawable/ic_star" + tools:ignore="RtlSymmetry" + tools:text="9.6" /> diff --git a/app/src/main/res/layout/navigation_header.xml b/app/src/main/res/layout/navigation_header.xml index f4acadd0d..4b3527d16 100644 --- a/app/src/main/res/layout/navigation_header.xml +++ b/app/src/main/res/layout/navigation_header.xml @@ -1,40 +1,43 @@ - + android:fitsSystemWindows="true"> - + + - - - - - - + android:layout_alignTop="@id/imageView_logo" + android:layout_alignBottom="@id/imageView_logo" + android:layout_marginStart="@dimen/nav_item_horizontal_padding" + android:layout_toEndOf="@id/imageView_logo" + android:gravity="center_vertical" + android:singleLine="true" + android:text="@string/app_name" + android:textAppearance="@style/TextAppearance.AppCompat.Title" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6b0ba74bd..a08cbf9eb 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -22,7 +22,8 @@ 120dp 34dp 16dp - 48dp + 36dp + 48dp 16dp From af20f65468465380e66a2dc3b9c26af507c89d4c Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Tue, 20 Jul 2021 00:06:22 +0000 Subject: [PATCH 028/180] Translated using Weblate (German) Currently translated at 88.6% (196 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ --- app/src/main/res/values-de/strings.xml | 116 ++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 30ccc909c..371d14364 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,5 +1,5 @@ - + Entfernen Möchtest du wirklich deinen gesamten Leseverlauf löschen\? Diese Aktion kann nicht rückgängig gemacht werden. Design @@ -82,4 +82,118 @@ Lokaler Speicher Menü öffnen Menü schließen + Ein Problem auf GitHub melden + Speicherort, um die Manga zu herunterladen + Die Manga, die du gerade liest, werden hier angezeigt + Am Anfang halten + An Breite anpassen + An die Höhe anpassen + Nützlich für AMOLED-Bildschirme + Schwarzer dunkler Modus + Neustart erforderlich + Du kannst den Lesemodus für jeden Manga separat einrichten + Von rechts nach links + Von rechts nach links-Lesemodus bevorzugen + Neue Kategorie + Sicherung und Wiederherstellung + Daten wiederhergestellt + Von Datensicherung wiederherstellen + Datensicherung erstellen + Datei nicht gefunden + Gerade jetzt + Gruppe + Suche nach neuen Kapiteln: %1$d von %2$d + Alle Cookies wurden entfernt + Cookies löschen + Standard: %s + Du solltest diesen Inhalt autorisieren, um ihn zu sehen + Umkehren + Neue Kapitel prüfen + Feed löschen + Anmelden + Das Passwort muss mindestens 4 Zeichen lang sein + Bestätigen + Gib das Passwort ein, das beim Starten der Anwendung benötigt wird + Nächste + Anderes + Möchtest du wirklich alle letzten Suchanfragen entfernen\? Diese Aktion kann nicht rückgängig gemacht werden. + Nur auf %s suchen + Symbolleiste beim Blättern ausblenden + Mehr erfahren + Einige Hersteller können das Systemverhalten ändern, wodurch Hintergrundaufgaben unterbrochen werden können. + Sicherung erfolgreich gespeichert + Beschreibung + Willkommen + Sprachen + Kategorie entfernen + Bildschirm drehen + Größe: %s + Neue Version: %s + Verwandte + Suchergebnisse + Hier siehst du die neuen Kapitel des Mangas, den du gerade liest + Später lesen + Diese Kategorie ist leer + Alle Favoriten + Fertig + Sichere Verbindung verwenden (HTTPS) + Sonstiger Speicherort + Verfügbarer Speicher kann nicht gefunden werden + Nicht verfügbar + Seitenanimation + Neueste Manga + Mangaregal + Du kannst es aus Online-Quellen speichern oder aus einer Datei importieren. + Du hast noch keinen Manga gespeichert + Was du lesen kannst, findest du im Seitenmenü. + Versuche, die Abfrage umzuformulieren. + Du kannst Kategorien verwenden, um deine Lieblingsmangas zu organisieren. Drücke +, um eine Kategorie zu erstellen + Umbenennen + Favoriten-Kategorien + Vibration + Leuchtanzeige + Benachrichtigungston + Einstellungen für Benachrichtigungen + Neu starten + Von Anfang an lesen + Herunterladen + Neue Kapitel + Aktiviert %1$d von %2$d + Benachrichtigungen + Manga speichern + Dieser Manga hat %s. Möchtest du alles davon speichern\? + Im Browser öffnen + Externer Speicher + Interner Speicher + Suchverlauf gelöscht + Suchverlauf löschen + Cache für Miniaturansichten löschen + Fehler + Nicht mehr fragen + Dieser Vorgang kann viel Netzwerkverkehr verbrauchen + Warnung + Weiter + Lautstärketasten + Seiten wechseln + Lesemoduseinstellungen + Manga löschen + Auf %s suchen + Rastergröße + Lesemodus + Webtoon + Standard + B|kB|MB|GB|TB + Cache + Seitencache löschen + Verlauf und Cache + Keine Beschreibung + Ungültige Datei. Nur ZIP und CBZ werden unterstützt. + Dieser Vorgang wird nicht unterstützt + Löschen + Importieren + Bild teilen + Seite erfolgreich gespeichert + Seite speichern + Warte, bis der Ladevorgang beendet ist + Löschen \ No newline at end of file From 149ac9280cd9531a5c289c08fd3a5765a2ddacf9 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Mon, 19 Jul 2021 21:34:33 +0000 Subject: [PATCH 029/180] Translated using Weblate (Italian) Currently translated at 90.9% (201 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ --- app/src/main/res/values-it/strings.xml | 133 ++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f9fef05f7..ac3f69bde 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,5 @@ - + Cronologia e cache Nessuna descrizione File non valido. Sono supportati solo ZIP e CBZ. @@ -70,4 +70,135 @@ Archiviazione locale Apri il menù Chiudi il menù + Leggi di più + Descrizione + Benvenuto/a + Lingue + Altro + Vuoi davvero rimuovere tutte le ricerche recenti\? Questa azione non può essere annullata. + Cerca solo su %s + Nascondi la barra degli strumenti quando si scorre + La password deve essere di almeno 4 caratteri + Conferma + Inserisci la password che sarà richiesta all\'avvio dell\'applicazione + Prossimo + … e %1$d altri + Accedi + Cancella il flusso + Silenzioso + Tocca per riprovare + Oggi + Gruppo + Molto tempo fa + Ieri + Proprio ora + File non trovato + Preparazione… + Dati ripristinati + Ripristina da un backup + Crea un backup dei dati + Backup e ripristino + Riavvio richiesto + Utile per gli schermi AMOLED + Tema nero scuro + Segnala un problema su GitHub + Nuova categoria + Puoi impostare la modalità di lettura per ogni manga separatamente + Preferisci la lettura da destra a sinistra + Da destra a sinistra + Nessun aggiornamento disponibile + Controllo dell\'aggiornamento fallito + Controllo degli aggiornamenti… + Controlla gli aggiornamenti + Versione %s + Informazioni + Le password non corrispondono + Ripeti la password + Chiedi la password all\'avvio dell\'applicazione + Proteggi l\'applicazione + Password sbagliata + Inserisci la password + Non controllare + Controlla gli aggiornamenti per il manga + L\'aggiornamento del flusso inizierà presto + Aggiorna + Ruota lo schermo + Flusso degli aggiornamenti cancellato + Cancella il flusso degli aggiornamenti + In attesa della rete… + Dimensioni: %s + Nuova versione: %s + Correlati + Risultati della ricerca + Qui potrai vedere i nuovi capitoli del manga che stai leggendo + Aggiornamenti + Leggi più tardi + Questa categoria è vuota + Tutti i preferiti + Finito + Usa una connessione sicura (HTTPS) + Altro spazio di archiviazione + Impossibile trovare uno spazio di archiviazione disponibile + Non disponibile + Posizione per lo scaricamento dei manga + Animazione delle pagine + Manga recenti + Scaffale da manga + Puoi salvarlo da fonti in linea o importarlo da un file. + Non hai ancora salvato nessun manga + Puoi trovare cosa leggere nel menù laterale. + I manga che stai leggendo saranno visualizzati qui + Prova a riformulare la domanda. + Puoi usare le categorie per organizzare i tuoi manga preferiti. Premi + per creare una categoria + È un po\' vuoto qui… + Rimuovi la categoria + Rinomina + Categorie… + Categorie di preferiti + Vibrazione + Indicatore luminoso + Suono di notifica + Impostazioni notifiche + Riavvia + Leggi dall\'inizio + Scarica + Notifica gli aggiornamenti dei manga che stai leggendo + Nuovi capitoli + Abilitato %1$d di %2$d + Notifiche + Salva il manga + Questo manga ha %s. Vuoi salvare tutto\? + Apri nel browser + Mostra una notifica se un aggiornamento è disponibile + Un aggiornamento per l\'applicazione è disponibile + Controlla gli aggiornamenti automaticamente + Dominio + Archiviazione esterna + Archiviazione interna + Solo gesti + Storia della ricerca cancellata + Cancella la cronologia delle ricerche + Svuota la cache delle miniature + Errore + Annullamento… + Non chiedere di nuovo + Questa operazione può consumare molto traffico di rete + Avviso + Continua + Pulsanti volume + Tocchi sui bordi + Cambia pagina + Impostazioni del lettore + Elimina il manga + Ricerca su %s + Dimensione della griglia + Modalità lettura + Webtoon + Standard + B|kB|MB|GB|TB + Cache + Cancella la cache delle pagine + Rimuovi + Vuoi davvero cancellare tutta la tua cronologia di lettura\? Questa azione non può essere annullata. + Cancella \ No newline at end of file From ec8c5e0fd488f9405adb12e300b7a16a75497041 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Mon, 19 Jul 2021 21:32:58 +0000 Subject: [PATCH 030/180] Translated using Weblate (French) Currently translated at 100.0% (221 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ --- app/src/main/res/values-fr/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 702bea500..0922b274a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -15,7 +15,7 @@ Confirmer Entrez le mot de passe qui sera demandé au démarrage de l\'application Suivant - … et %1$d autres + … et %1$d autre(s) Par défaut : %s Vous devez autoriser la visualisation de ce contenu Se connecter @@ -48,7 +48,7 @@ Redémarrage nécessaire Utile pour les écrans AMOLED Thème noir foncé - Garder au départ + Garder au début Ajuster à la largeur Ajuster à la hauteur Ajuster au centre From c59e3165b6ebc33849f82178ae30c0c40cb2dda7 Mon Sep 17 00:00:00 2001 From: HelaBasa Date: Tue, 20 Jul 2021 18:16:31 +0000 Subject: [PATCH 031/180] Translated using Weblate (Sinhala) Currently translated at 4.0% (9 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/si/ --- app/src/main/res/values-si/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index a6b3daec9..a14534dae 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -1,2 +1,10 @@ - \ No newline at end of file + + පූරණය වෙමින්… + සැකසුම් + පරිච්ඡේද + විස්තර + දෝෂයක් සිදුවී ඇත + ඉතිහාසය + ප්‍රියතමයන් + \ No newline at end of file From 73efe6fd838827b1ab4bebbbc1f9739cf60b40a3 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 23 Jul 2021 03:03:04 +0000 Subject: [PATCH 032/180] Translated using Weblate (German) Currently translated at 100.0% (221 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ --- app/src/main/res/values-de/strings.xml | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 371d14364..3f1415a5d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -87,7 +87,7 @@ Die Manga, die du gerade liest, werden hier angezeigt Am Anfang halten An Breite anpassen - An die Höhe anpassen + An Höhe anpassen Nützlich für AMOLED-Bildschirme Schwarzer dunkler Modus Neustart erforderlich @@ -196,4 +196,29 @@ Seite speichern Warte, bis der Ladevorgang beendet ist Löschen + Auf Kanten tippen + Löse + CAPTCHA ist erforderlich + Stumm + Die gewählte Konfiguration wird für diesen Manga gespeichert + Tippe, um es erneut zu versuchen + Heute + Vor langer Zeit + Gestern + Du kannst eine Sicherung deines Verlaufs und deiner Favoriten erstellen und diese wiederherstellen + Die Daten wurden wiederhergestellt, aber es gibt Fehler + Alle Daten erfolgreich wiederhergestellt + An Zentrum anpassen + Skalierungsmodus + Version %s + Über + Passwörter stimmen nicht überein + Wiederhole das Passwort + Beim Start der Anwendung nach dem Passwort fragen + Anwendung schützen + Falsches Passwort + Gib das Passwort ein + Nicht prüfen + Domäne + Nur Gesten \ No newline at end of file From 2e5afc73e7b0773d0946600e874e9242325d7a8a Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 23 Jul 2021 03:07:38 +0000 Subject: [PATCH 033/180] Translated using Weblate (Italian) Currently translated at 100.0% (221 of 221 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ --- app/src/main/res/values-it/strings.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ac3f69bde..97f5eaf8e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -201,4 +201,24 @@ Rimuovi Vuoi davvero cancellare tutta la tua cronologia di lettura\? Questa azione non può essere annullata. Cancella + Alcuni produttori possono cambiare il comportamento del sistema, che può interrompere le attività nello sfondo. + Backup salvato con successo + Predefinito: %s + Devi autorizzare la visualizzazione di questo contenuto + Inverti + Controllo dei nuovi capitoli + Controllo dei nuovi capitoli: %1$d di %2$d + Tutti i cookie sono stati rimossi + Cancella i cookie + Risolvi + CAPTCHA è richiesto + La configurazione scelta sarà ricordata per questo manga + Puoi creare un backup della tua cronologia e dei tuoi preferiti e ripristinarlo + I dati sono ripristinati, ma ci sono errori + Tutti i dati ripristinati con successo + Tieni all\'inizio + Adatta all\'altezza + Adatta al centro + Adatta alla larghezza + Modalità scala \ No newline at end of file From ad79ff27399d099d6fa68fa9083820bdf42b93e4 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 24 Jul 2021 11:10:29 +0000 Subject: [PATCH 034/180] Translated using Weblate (Belarusian) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ --- app/src/main/res/values-be/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 54fcffb50..d68b19691 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -219,4 +219,8 @@ Тут будзе адлюстроўвацца манга, якую вы чытаеце Паспрабуйце перафармуляваць запыт. Тут неяк пуста… + Частка адсутнічае + Гэтая частка адсутнічае на вашым прыладзе. Загрузіце яе або прачытайце онлайн + На дадзены момант няма актыўных загрузак + У чарзе \ No newline at end of file From c67ce383506c9646bd1ce414b900fefb1c5008eb Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 24 Jul 2021 08:07:04 +0000 Subject: [PATCH 035/180] Translated using Weblate (Spanish) Currently translated at 93.3% (210 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/ --- app/src/main/res/values-es/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4771f1165..72a0f75e1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -203,4 +203,9 @@ Introduce la contraseña que se requerirá cuando se inicie la aplicación Confirmar La contraseña debe tener al menos 4 caracteres + Puedes guardarlo desde fuentes in línea o importarlo desde un archivo. + Todavía no tienes ningún manga guardado + Puede encontrar qué leer en el menú lateral. + El manga que estás leyendo se mostrará aquí + Está un poco vacío aquí… \ No newline at end of file From 34ad0a7c68bbcca02a4a073d2d43662be16b8e5c Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 24 Jul 2021 11:11:52 +0000 Subject: [PATCH 036/180] Translated using Weblate (Russian) Currently translated at 99.5% (224 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ --- app/src/main/res/values-ru/strings.xml | 443 +++++++++++++------------ 1 file changed, 223 insertions(+), 220 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 70c4cd7ab..35cfc2e73 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,222 +1,225 @@ + - Закрыть меню - Открыть меню - На устройстве - Избранное - История - Произошла ошибка - Ошибка сети - Подробности - Главы - Список - Подробный список - Таблица - Вид списка - Настройки - Онлайн каталоги - Загрузка… - Глава %1$d из %2$d - Закрыть - Повторить - Очистить историю - Ничего не найдено - История пуста - Читать - Добавить закладку - Добавьте интересующую Вас мангу в избранное, чтобы не потерять её - В избранное - Создать категорию - Добавить - Введите название - Сохранить - Поделиться - Создать ярлык… - Поделиться %s - Поиск - Поиск манги - Загрузка манги… - Обработка… - Загрузка завершена - Загрузки - По имени - Популярная - Обновлённая - Новая - По рейтингу - Все - Сортировка - Жанр - Фильтр - Тема - Светлая - Тёмная - Автоматически - Страницы - Очистить - Вы уверены, что хотите очистить историю? Это действие нельзя будет отменить. - Удалить - \"%s\" удалено из истории - \"%s\" удалено с устройства - Дождитесь окончания загрузки - Сохранить страницу - Страница сохранена - Поделиться изображением - Импорт - Удалить - Операция не поддерживается - Поддерживаются только файлы ZIP и CBZ. - Нет описания - История и кэш - Очистить кэш страниц - Кэш - Б|кБ|МБ|ГБ|ТБ - Стандартный - Манхва - Режим чтения - Размер таблицы - Поиск по %s - Удалить мангу - Вы уверены, что хотите удалить \"%s\" с устройства? \nЭто действие нельзя будет отменить. - Настройки чтения - Листание страниц - Нажатия по краям - Кнопки громкости - Продолжить - Предупреждение - Данная операция может привести к большому расходу траффика - Больше не спрашивать - Отмена… - Ошибка - Очистить кэш миниатюр - Очистить историю поиска - История поиска очищена - Только жесты - Внутренний накопитель - Внешнее хранилище - Домен - Проверять обновление приложения - Доступно обновление приложения - Показывать уведомление при наличии новой версии - Открыть в браузере - В этой манге %s. Вы уверены, что хотите сохранить их все? - Сохранить мангу - Уведомления - Включено %1$d из %2$d - Новые главы - Уведомлять об обновлении манги, которую Вы читаете - Загрузить - Читать с начала - Перезапустить - Настройки уведомлений - Звук уведомления - Световая индикация - Вибросигнал - Категории избранного - Категории… - Переименовать - Вы уверены, что хотите удалить категорию \"%s\"? \nВся манга из данной категории будет утеряна. - Удалить категорию - Как-то здесь пусто… - Попробуйте переформулировать запрос. - Категории помогают упорядочивать избранную мангу. Нажмите «+», чтобы создать категорию - Здесь будет отображаться манга, которую Вы читаете - Вы можете найти, что почитать, в боковом меню. - У Вас пока нет сохранённой манги - Вы можете сохранить мангу из онлайн каталога или импортировать из файла. - Полка с мангой - Недавняя манга - Анимация листания - Место сохранения манги - Недоступно - Не удалось найти ни одного доступного хранилища - Другое хранилище - Защищённое соединение (HTTPS) - Готово - Всё избранное - В этой категории ничего нет - Прочитать позже - Обновления - Здесь будут отображаться обновления манги, которую Вы читаете - Результаты поиска - Похожие - Новая версия: %s - Размер: %s - Ожидание подключения… - Очистить ленту обновлений - Лента обновлений очищена - Повернуть экран - Обновить - Обновление скоро начнётся - Проверять обновления манги - Не проверять - Введите пароль - Неверный пароль - Защитить приложение - Запрашивать пароль при запуске приложения - Повторите пароль - Пароли не совпадают - О программе - Версия %s - Проверить обновления - Проверка обновления… - Ошибка при проверке обновления - Нет доступных обновлений - Справа налево - Предпочитать режим Справа налево - Вы можете настроить режим чтения для каждой манги отдельно - Создать категорию - Масштабирование - Вписать в экран - Подогнать по высоте - Подогнать по ширине - Исходный размер - Чёрная тёмная тема - Полезно для AMOLED экранов - Требуется перезапуск - Резервное копирование - Создать резервную копию - Восстановить данные - Данные восстановлены - Подготовка… - Файл не найден - Все данные успешно восстановлены - Данные восстановлены, но возникли некоторые ошибки - Вы можете создать резервную копию избранного и истории и потом восстановить их - Только что - Вчера - Давно - Группировать - Сегодня - Попробовать ещё раз - Выбранный режим будет сохранён для текущей манги - Без звука - Необходимо пройти CAPTCHA - Пройти - Очистить куки - Все куки удалены - Проверка новых глав: %1$d из %2$d - Очистить ленту - Вся история обновлений будет очищена и её нельзя будет вернуть. Вы уверены? - Проверка новых глав - В обратном порядке - Войти - Для просмотра этого контента требуется авторизация - По умолчанию: %s - …и ещё %1$d - Далее - Введите пароль, который вам понадобится при запуске приложения - Подтвердить - Пароль должен содержать не менее 4 символов - Прятать заголовок при прокрутке - Поиск только по %s - Другие - Описание - Языки - Добро пожаловать - Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. - Резервная копия успешно сохранена - Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. - Подробнее - В очереди - На данный момент нет активных загрузок + Закрыть меню + Открыть меню + На устройстве + Избранное + История + Произошла ошибка + Ошибка сети + Подробности + Главы + Список + Подробный список + Таблица + Вид списка + Настройки + Онлайн каталоги + Загрузка… + Глава %1$d из %2$d + Закрыть + Повторить + Очистить историю + Ничего не найдено + История пуста + Читать + Добавить закладку + Добавьте интересующую Вас мангу в избранное, чтобы не потерять её + В избранное + Создать категорию + Добавить + Введите название + Сохранить + Поделиться + Создать ярлык… + Поделиться %s + Поиск + Поиск манги + Загрузка манги… + Обработка… + Загрузка завершена + Загрузки + По имени + Популярная + Обновлённая + Новая + По рейтингу + Все + Сортировка + Жанр + Фильтр + Тема + Светлая + Тёмная + Автоматически + Страницы + Очистить + Вы уверены, что хотите очистить историю? Это действие нельзя будет отменить. + Удалить + \"%s\" удалено из истории + \"%s\" удалено с устройства + Дождитесь окончания загрузки + Сохранить страницу + Страница сохранена + Поделиться изображением + Импорт + Удалить + Операция не поддерживается + Поддерживаются только файлы ZIP и CBZ. + Нет описания + История и кэш + Очистить кэш страниц + Кэш + Б|кБ|МБ|ГБ|ТБ + Стандартный + Манхва + Режим чтения + Размер таблицы + Поиск по %s + Удалить мангу + Вы уверены, что хотите удалить \"%s\" с устройства? \nЭто действие нельзя будет отменить. + Настройки чтения + Листание страниц + Нажатия по краям + Кнопки громкости + Продолжить + Предупреждение + Данная операция может привести к большому расходу траффика + Больше не спрашивать + Отмена… + Ошибка + Очистить кэш миниатюр + Очистить историю поиска + История поиска очищена + Только жесты + Внутренний накопитель + Внешнее хранилище + Домен + Проверять обновление приложения + Доступно обновление приложения + Показывать уведомление при наличии новой версии + Открыть в браузере + В этой манге %s. Вы уверены, что хотите сохранить их все? + Сохранить мангу + Уведомления + Включено %1$d из %2$d + Новые главы + Уведомлять об обновлении манги, которую Вы читаете + Загрузить + Читать с начала + Перезапустить + Настройки уведомлений + Звук уведомления + Световая индикация + Вибросигнал + Категории избранного + Категории… + Переименовать + Вы уверены, что хотите удалить категорию \"%s\"? \nВся манга из данной категории будет утеряна. + Удалить категорию + Как-то здесь пусто… + Попробуйте переформулировать запрос. + Категории помогают упорядочивать избранную мангу. Нажмите «+», чтобы создать категорию + Здесь будет отображаться манга, которую Вы читаете + Вы можете найти, что почитать, в боковом меню. + У Вас пока нет сохранённой манги + Вы можете сохранить мангу из онлайн каталога или импортировать из файла. + Полка с мангой + Недавняя манга + Анимация листания + Место сохранения манги + Недоступно + Не удалось найти ни одного доступного хранилища + Другое хранилище + Защищённое соединение (HTTPS) + Готово + Всё избранное + В этой категории ничего нет + Прочитать позже + Обновления + Здесь будут отображаться обновления манги, которую Вы читаете + Результаты поиска + Похожие + Новая версия: %s + Размер: %s + Ожидание подключения… + Очистить ленту обновлений + Лента обновлений очищена + Повернуть экран + Обновить + Обновление скоро начнётся + Проверять обновления манги + Не проверять + Введите пароль + Неверный пароль + Защитить приложение + Запрашивать пароль при запуске приложения + Повторите пароль + Пароли не совпадают + О программе + Версия %s + Проверить обновления + Проверка обновления… + Ошибка при проверке обновления + Нет доступных обновлений + Справа налево + Предпочитать режим Справа налево + Вы можете настроить режим чтения для каждой манги отдельно + Создать категорию + Масштабирование + Вписать в экран + Подогнать по высоте + Подогнать по ширине + Исходный размер + Чёрная тёмная тема + Полезно для AMOLED экранов + Требуется перезапуск + Резервное копирование + Создать резервную копию + Восстановить данные + Данные восстановлены + Подготовка… + Файл не найден + Все данные успешно восстановлены + Данные восстановлены, но возникли некоторые ошибки + Вы можете создать резервную копию избранного и истории и потом восстановить их + Только что + Вчера + Давно + Группировать + Сегодня + Попробовать ещё раз + Выбранный режим будет сохранён для текущей манги + Без звука + Необходимо пройти CAPTCHA + Пройти + Очистить куки + Все куки удалены + Проверка новых глав: %1$d из %2$d + Очистить ленту + Вся история обновлений будет очищена и её нельзя будет вернуть. Вы уверены? + Проверка новых глав + В обратном порядке + Войти + Для просмотра этого контента требуется авторизация + По умолчанию: %s + …и ещё %1$d + Далее + Введите пароль, который вам понадобится при запуске приложения + Подтвердить + Пароль должен содержать не менее 4 символов + Прятать заголовок при прокрутке + Поиск только по %s + Другие + Описание + Языки + Добро пожаловать + Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. + Резервная копия успешно сохранена + Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. + Подробнее + В очереди + На данный момент нет активных загрузок + Глава отсутствует + Эта глава отсутствует на вашем устройстве. Скачайте её или прочитайте онлайн \ No newline at end of file From 05bbfe77b2b76e9f14fcd7bea516d8f511247442 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 24 Jul 2021 08:05:13 +0000 Subject: [PATCH 037/180] Translated using Weblate (German) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ --- app/src/main/res/values-de/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3f1415a5d..b4bf7a2a0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -221,4 +221,8 @@ Nicht prüfen Domäne Nur Gesten + Kapitel fehlt + Dieses Kapitel fehlt auf deinem Gerät. Lade ihn herunter oder lese ihn online + Zurzeit sind keine aktiven Datenübertragungen vorhanden + In Warteschlange \ No newline at end of file From e2608cf85a4d8f509766424ba8771307cc10e21a Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 24 Jul 2021 08:02:05 +0000 Subject: [PATCH 038/180] Translated using Weblate (Italian) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ --- app/src/main/res/values-it/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 97f5eaf8e..22fb8ce94 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -221,4 +221,8 @@ Adatta al centro Adatta alla larghezza Modalità scala + Capitolo mancante + Questo capitolo manca sul tuo dispositivo. Scaricalo o leggilo in linea + Attualmente non ci sono scaricamenti attivi + In coda \ No newline at end of file From 45e1502c9bd13e5314524a6c2a3dbaeb9484491a Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Sat, 24 Jul 2021 08:00:09 +0000 Subject: [PATCH 039/180] Translated using Weblate (French) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ --- app/src/main/res/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0922b274a..2777aac39 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -221,4 +221,8 @@ Stockage local Ouvrir le menu Fermer le menu + Chapitre manquant + Ce chapitre est manquant sur votre appareil. Téléchargez-le ou lisez-le en ligne + Il n\'y a actuellement aucun téléchargement actif + En file d\'attente \ No newline at end of file From 2ac1828a0ce2f23ed9597191a3aaccfe5851b3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Tue, 27 Jul 2021 22:37:32 +0000 Subject: [PATCH 040/180] Translated using Weblate (English) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/ --- app/src/main/res/values/strings.xml | 451 ++++++++++++++-------------- 1 file changed, 226 insertions(+), 225 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a974ac5c..3c354bd6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,227 +1,228 @@ + - Kotatsu - - Close menu - Open menu - Local storage - Favourites - History - An error has occurred - Network connection error - Details - Chapters - List - Detailed list - Grid - List mode - Settings - Remote sources - Loading… - Chapter %1$d of %2$d - Close - Try again - Clear history - Nothing found - History is empty - Read - Add bookmark - You have not favourites yet - Add to favourites - Add new category - Add - Enter category name - Save - Share - Create shortcut… - Share %s - Search - Search manga - Manga downloading… - Processing… - Download complete - Downloads - By name - Popular - Updated - Newest - By rating - All - Sort order - Genre - Filter - Theme - Light - Dark - Automatic - Pages - Clear - Do you really want to clear all your reading history? This action cannot be undone. - Remove - \"%s\" removed from history - \"%s\" deleted from local storage - Wait for the load to finish - Save page - Page saved successful - Share image - Import - Delete - This operation is not supported - Invalid file. Only ZIP and CBZ are supported. - No description - History and cache - Clear pages cache - Cache - B|kB|MB|GB|TB - Standard - Webtoon - Read mode - Grid size - Search on %s - Delete manga - Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone. - Reader settings - Switch pages - Taps on edges - Volume buttons - Continue - Warning - This operation may consume a lot of network traffic - Don`t ask again - Cancelling… - Error - Clear thumbnails cache - Clear search history - Search history cleared - Gestures only - Internal storage - External storage - Domain - Check for updates automatically - Application update is available - Show notification if update is available - Open in browser - This manga has %s. Do you want to save all of it? - Save manga - Notifications - Enabled %1$d of %2$d - New chapters - Notify about updates of manga you are reading - Download - Read from start - Restart - Notifications settings - Notification sound - Light indicator - Vibration - Favourites categories - Categories… - Rename - Do you really want to remove category \"%s\" from your favourites? \nAll containing manga will be lost. - Remove category - It\'s kind of empty here… - You can use categories to organize your favourite manga. Press «+» to create a category - Try to reformulate the query. - Manga you are reading will be displayed here - You can find what to read in side menu. - You have not any saved manga yet - You can save it from online sources or import from file. - Manga shelf - Recent manga - Pages animation - Manga download location - Not available - Cannot find any available storage - Other storage - Use secure connection (HTTPS) - Done - All favourites - This category is empty - Read later - Updates - Here you will see the new chapters of the manga you are reading - Search results - Related - New version: %s - Size: %s - Waiting for network… - Clear updates feed - Updates feed cleared - Rotate screen - Update - Feed update will start soon - Check updates for manga - Don`t check - Enter password - Wrong password - Protect application - Ask for password on application start - Repeat password - Passwords do not match - About - Version %s - Check for updates - Checking for updates… - Update check failed - No updates available - Right to left - Prefer Right to left reader - You can set up the reading mode for each manga separately - New category - Create issue on GitHub - Scale mode - Fit center - Fit to height - Fit to width - Keep at start - Black dark theme - Useful for AMOLED screens - Restart required - - Create data backup - Restore from backup - Data restored - Preparing… - File not found - All data restored successfully - The data restored, but there are errors - You can create backup of your history and favourites and restore it - Just now - Yesterday - Long ago - Group - Today - Tap to try again - Chosen configuration will be remembered for this manga - Silent - CAPTCHA is required - Solve - Clear cookies - All cookies was removed - Checking for new chapters: %1$d of %2$d - Clear feed - All updates history will be cleared and this action cannot be undone. Are you sure? - New chapters checking - Reverse - Sign in - You should authorize to view this content - Default: %s - …and %1$d more - Next - Enter password that will be required when the application starts - Confirm - Password must be at least 4 characters - Hide toolbar when scrolling - Search only on %s - Do you really want to remove all recent search queries? This action cannot be undone. - Other - Languages - Welcome - Description - Backup saved successfully - Some manufacturers can change the system behavior, which may breaks background tasks. - Read more - Queued - There are currently no active downloads - This chapter is missing on your device. Download it or read online - Chapter is missing + Kotatsu + + Close menu + Open menu + Local storage + Favourites + History + An error has occurred + Network connection error + Details + Chapters + List + Detailed list + Grid + List mode + Settings + Remote sources + Loading… + Chapter %1$d of %2$d + Close + Try again + Clear history + Nothing found + History is empty + Read + Add bookmark + You have not favourites yet + Add to favourites + Add new category + Add + Enter category name + Save + Share + Create shortcut… + Share %s + Search + Search manga + Manga downloading… + Processing… + Download complete + Downloads + By name + Popular + Updated + Newest + By rating + All + Sort order + Genre + Filter + Theme + Light + Dark + Automatic + Pages + Clear + Do you really want to clear all your reading history? This action cannot be undone. + Remove + \"%s\" removed from history + \"%s\" deleted from local storage + Wait for the load to finish + Save page + Page saved successful + Share image + Import + Delete + This operation is not supported + Invalid file. Only ZIP and CBZ are supported. + No description + History and cache + Clear pages cache + Cache + B|kB|MB|GB|TB + Standard + Webtoon + Read mode + Grid size + Search on %s + Delete manga + Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone. + Reader settings + Switch pages + Taps on edges + Volume buttons + Continue + Warning + This operation may consume a lot of network traffic + Don`t ask again + Cancelling… + Error + Clear thumbnails cache + Clear search history + Search history cleared + Gestures only + Internal storage + External storage + Domain + Check for updates automatically + Application update is available + Show notification if update is available + Open in browser + This manga has %s. Do you want to save all of it? + Save manga + Notifications + Enabled %1$d of %2$d + New chapters + Notify about updates of manga you are reading + Download + Read from start + Restart + Notifications settings + Notification sound + Light indicator + Vibration + Favourites categories + Categories… + Rename + Do you really want to remove category \"%s\" from your favourites? \nAll containing manga will be lost. + Remove category + It\'s kind of empty here… + You can use categories to organize your favourite manga. Press «+» to create a category + Try to reformulate the query. + Manga you are reading will be displayed here + You can find what to read in side menu. + You have not any saved manga yet + You can save it from online sources or import from file. + Manga shelf + Recent manga + Pages animation + Manga download location + Not available + Cannot find any available storage + Other storage + Use secure connection (HTTPS) + Done + All favourites + This category is empty + Read later + Updates + Here you will see the new chapters of the manga you are reading + Search results + Related + New version: %s + Size: %s + Waiting for network… + Clear updates feed + Updates feed cleared + Rotate screen + Update + Feed update will start soon + Check updates for manga + Don`t check + Enter password + Wrong password + Protect application + Ask for password on application start + Repeat password + Passwords do not match + About + Version %s + Check for updates + Checking for updates… + Update check failed + No updates available + Right to left + Prefer Right to left reader + You can set up the reading mode for each manga separately + New category + Create issue on GitHub + Scale mode + Fit center + Fit to height + Fit to width + Keep at start + Black dark theme + Useful for AMOLED screens + Restart required + + Create data backup + Restore from backup + Data restored + Preparing… + File not found + All data restored successfully + The data restored, but there are errors + You can create backup of your history and favourites and restore it + Just now + Yesterday + Long ago + Group + Today + Tap to try again + Chosen configuration will be remembered for this manga + Silent + CAPTCHA is required + Solve + Clear cookies + All cookies was removed + Checking for new chapters: %1$d of %2$d + Clear feed + All updates history will be cleared and this action cannot be undone. Are you sure? + New chapters checking + Reverse + Sign in + You should authorize to view this content + Default: %s + …and %1$d more + Next + Enter password that will be required when the application starts + Confirm + Password must be at least 4 characters + Hide toolbar when scrolling + Search only on %s + Do you really want to remove all recent search queries? This action cannot be undone. + Other + Languages + Welcome + Description + Backup saved successfully + Some manufacturers can change the system behavior, which may breaks background tasks. + Read more + Queued + There are currently no active downloads + This chapter is missing on your device. Download or read it online. + Chapter is missing \ No newline at end of file From 9c55fd166e1c80d914de5d7a1713acf0f1667ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Tue, 27 Jul 2021 22:37:09 +0000 Subject: [PATCH 041/180] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 86.6% (195 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index f0f089cbf..2289f397e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -219,4 +219,8 @@ Lokallagring Åpne meny Lukk meny + Dette kapitlet mangler på din enhet. Last det ned eller les det på nett. + Kapittel mangler + Det er ingen aktive nedlastinger + I kø \ No newline at end of file From fbd0f25b8f9e7ff5af30e04c3aba73501318d88f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 4 Aug 2021 08:32:20 +0300 Subject: [PATCH 042/180] #8 Configure sort order for each favourites category --- app/build.gradle | 8 ++-- .../kotatsu/core/backup/BackupRepository.kt | 1 + .../kotatsu/core/backup/RestoreRepository.kt | 4 +- .../kotatsu/core/db/DatabaseModule.kt | 1 + .../koitharu/kotatsu/core/db/MangaDatabase.kt | 2 +- .../core/db/migrations/Migration8To9.kt | 12 ++++++ .../kotatsu/core/model/FavouriteCategory.kt | 3 +- .../favourites/data/FavouriteCategoriesDao.kt | 11 ++++-- .../data/FavouriteCategoryEntity.kt | 7 +++- .../kotatsu/favourites/data/FavouritesDao.kt | 36 +++++++++++++++--- .../favourites/domain/FavouritesRepository.kt | 37 +++++++++++++------ .../ui/FavouritesContainerFragment.kt | 29 +++++++++++++-- .../favourites/ui/FavouritesPagerAdapter.kt | 5 ++- .../ui/categories/CategoriesActivity.kt | 32 +++++++++++++++- .../ui/categories/CategoriesAdapter.kt | 21 +++++++++-- .../FavouritesCategoriesViewModel.kt | 14 +++++-- .../ui/list/FavouritesListViewModel.kt | 7 +++- .../koitharu/kotatsu/widget/WidgetUpdater.kt | 3 +- app/src/main/res/menu/popup_category.xml | 13 +++++++ 19 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt diff --git a/app/build.gradle b/app/build.gradle index d8fb63356..895e66295 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,19 +70,19 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.activity:activity-ktx:1.2.3' - implementation 'androidx.fragment:fragment-ktx:1.3.5' + implementation 'androidx.activity:activity-ktx:1.3.0' + implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-service:2.3.1' implementation 'androidx.lifecycle:lifecycle-process:2.3.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference-ktx:1.1.1' - implementation 'androidx.work:work-runtime-ktx:2.6.0-beta01' + implementation 'androidx.work:work-runtime-ktx:2.6.0-beta02' implementation 'com.google.android.material:material:1.4.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 6d934330a..eb6cb8579 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -118,6 +118,7 @@ class BackupRepository(private val db: MangaDatabase) { jo.put("created_at", createdAt) jo.put("sort_key", sortKey) jo.put("title", title) + jo.put("order", order) return jo } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt index 86b732b27..9626ddb13 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt @@ -5,6 +5,7 @@ import org.json.JSONObject import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.history.data.HistoryEntity @@ -101,7 +102,8 @@ class RestoreRepository(private val db: MangaDatabase) { categoryId = json.getInt("category_id"), createdAt = json.getLong("created_at"), sortKey = json.getInt("sort_key"), - title = json.getString("title") + title = json.getString("title"), + order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name, ) private fun parseFavourite(json: JSONObject) = FavouriteEntity( diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt index de6aead18..dc390f0f3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt @@ -20,6 +20,7 @@ val databaseModule Migration5To6(), Migration6To7(), Migration7To8(), + Migration8To9(), ).addCallback( DatabasePrePopulateCallback(androidContext().resources) ).build() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index f0844571e..04b1ba764 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -16,7 +16,7 @@ import org.koitharu.kotatsu.history.data.HistoryEntity MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class - ], version = 8 + ], version = 9 ) abstract class MangaDatabase : RoomDatabase() { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt new file mode 100644 index 000000000..84eedc797 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/migrations/Migration8To9.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.koitharu.kotatsu.core.model.SortOrder + +class Migration8To9 : Migration(8, 9) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt index 5eedf24b8..6998a1a08 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/FavouriteCategory.kt @@ -9,5 +9,6 @@ data class FavouriteCategory( val id: Long, val title: String, val sortKey: Int, - val createdAt: Date + val order: SortOrder, + val createdAt: Date, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index e56cf6999..436dc12ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.favourites.data import androidx.room.* import kotlinx.coroutines.flow.Flow -import org.koitharu.kotatsu.core.model.FavouriteCategory @Dao abstract class FavouriteCategoriesDao { @@ -13,6 +12,9 @@ abstract class FavouriteCategoriesDao { @Query("SELECT * FROM favourite_categories ORDER BY sort_key") abstract fun observeAll(): Flow> + @Query("SELECT * FROM favourite_categories WHERE category_id = :id") + abstract fun observe(id: Long): Flow + @Insert(onConflict = OnConflictStrategy.ABORT) abstract suspend fun insert(category: FavouriteCategoryEntity): Long @@ -23,10 +25,13 @@ abstract class FavouriteCategoriesDao { abstract suspend fun delete(id: Long) @Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id") - abstract suspend fun update(id: Long, title: String) + abstract suspend fun updateTitle(id: Long, title: String) + + @Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id") + abstract suspend fun updateOrder(id: Long, order: String) @Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id") - abstract suspend fun update(id: Long, sortKey: Int) + abstract suspend fun updateSortKey(id: Long, sortKey: Int) @Query("SELECT MAX(sort_key) FROM favourite_categories") protected abstract suspend fun getMaxSortKey(): Int? diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt index 1da715ae5..f66c87fae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.model.SortOrder import java.util.* @Entity(tableName = "favourite_categories") @@ -12,13 +13,15 @@ data class FavouriteCategoryEntity( @ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "sort_key") val sortKey: Int, - @ColumnInfo(name = "title") val title: String + @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "order") val order: String, ) { fun toFavouriteCategory(id: Long? = null) = FavouriteCategory( id = id ?: categoryId.toLong(), title = title, sortKey = sortKey, - createdAt = Date(createdAt) + order = SortOrder.values().find { x -> x.name == order } ?: SortOrder.NEWEST, + createdAt = Date(createdAt), ) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 7928ba470..cf7d0f8f9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -1,8 +1,11 @@ package org.koitharu.kotatsu.favourites.data import androidx.room.* +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.model.SortOrder @Dao abstract class FavouritesDao { @@ -11,9 +14,13 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC") abstract suspend fun findAll(): List - @Transaction - @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC") - abstract fun observeAll(): Flow> + fun observeAll(order: SortOrder): Flow> { + val orderBy = getOrderBy(order) + val query = SimpleSQLiteQuery( + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id GROUP BY favourites.manga_id ORDER BY $orderBy", + ) + return observeAllRaw(query) + } @Transaction @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset") @@ -23,9 +30,14 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC") abstract suspend fun findAll(categoryId: Long): List - @Transaction - @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC") - abstract fun observeAll(categoryId: Long): Flow> + fun observeAll(categoryId: Long, order: SortOrder): Flow> { + val orderBy = getOrderBy(order) + val query = SimpleSQLiteQuery( + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id WHERE category_id = ? GROUP BY favourites.manga_id ORDER BY $orderBy", + arrayOf(categoryId), + ) + return observeAllRaw(query) + } @Transaction @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset") @@ -63,4 +75,16 @@ abstract class FavouritesDao { insert(entity) } } + + @Transaction + @RawQuery(observedEntities = [FavouriteEntity::class]) + protected abstract fun observeAllRaw(query: SupportSQLiteQuery): Flow> + + private fun getOrderBy(sortOrder: SortOrder) = when(sortOrder) { + SortOrder.RATING -> "rating DESC" + SortOrder.NEWEST, + SortOrder.UPDATED -> "created_at DESC" + SortOrder.ALPHABETICAL -> "title ASC" + else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 15cefac92..48d6a34aa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -3,12 +3,14 @@ package org.koitharu.kotatsu.favourites.domain import androidx.room.withTransaction import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.utils.ext.mapItems @@ -21,26 +23,26 @@ class FavouritesRepository(private val db: MangaDatabase) { return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } - fun observeAll(): Flow> { - return db.favouritesDao.observeAll() + fun observeAll(order: SortOrder): Flow> { + return db.favouritesDao.observeAll(order) .mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } - suspend fun getAllManga(offset: Int): List { - val entities = db.favouritesDao.findAll(offset, 20) - return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } - } - suspend fun getManga(categoryId: Long): List { val entities = db.favouritesDao.findAll(categoryId) return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } - fun observeAll(categoryId: Long): Flow> { - return db.favouritesDao.observeAll(categoryId) + fun observeAll(categoryId: Long, order: SortOrder): Flow> { + return db.favouritesDao.observeAll(categoryId, order) .mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } } + fun observeAll(categoryId: Long): Flow> { + return observeOrder(categoryId) + .flatMapLatest { order -> observeAll(categoryId, order) } + } + suspend fun getManga(categoryId: Long, offset: Int): List { val entities = db.favouritesDao.findAll(categoryId, offset, 20) return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } @@ -77,25 +79,30 @@ class FavouritesRepository(private val db: MangaDatabase) { title = title, createdAt = System.currentTimeMillis(), sortKey = db.favouriteCategoriesDao.getNextSortKey(), - categoryId = 0 + categoryId = 0, + order = SortOrder.UPDATED.name, ) val id = db.favouriteCategoriesDao.insert(entity) return entity.toFavouriteCategory(id) } suspend fun renameCategory(id: Long, title: String) { - db.favouriteCategoriesDao.update(id, title) + db.favouriteCategoriesDao.updateTitle(id, title) } suspend fun removeCategory(id: Long) { db.favouriteCategoriesDao.delete(id) } + suspend fun setCategoryOrder(id: Long, order: SortOrder) { + db.favouriteCategoriesDao.updateOrder(id, order.name) + } + suspend fun reorderCategories(orderedIds: List) { val dao = db.favouriteCategoriesDao db.withTransaction { for ((i, id) in orderedIds.withIndex()) { - dao.update(id, i) + dao.updateSortKey(id, i) } } } @@ -117,4 +124,10 @@ class FavouritesRepository(private val db: MangaDatabase) { suspend fun removeFromFavourites(manga: Manga) { db.favouritesDao.delete(manga.id) } + + private fun observeOrder(categoryId: Long): Flow { + return db.favouriteCategoriesDao.observe(categoryId) + .map { x -> SortOrder.values().find { it.name == x.order } ?: SortOrder.NEWEST } + .distinctUntilChanged() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index c20c2c6f3..43ac54837 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -11,6 +11,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate @@ -19,7 +20,6 @@ import org.koitharu.kotatsu.utils.RecycledViewPoolHolder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.showPopupMenu import java.util.* -import kotlin.collections.ArrayList class FavouritesContainerFragment : BaseFragment(), FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback, @@ -100,11 +100,19 @@ class FavouritesContainerFragment : BaseFragment(), override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean { val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category - tabView.showPopupMenu(menuRes) { + tabView.showPopupMenu(menuRes, { menu -> + createOrderSubmenu(menu, category) + }) { when (it.itemId) { R.id.action_remove -> editDelegate.deleteCategory(category) R.id.action_rename -> editDelegate.renameCategory(category) R.id.action_create -> editDelegate.createCategory() + R.id.action_order -> return@showPopupMenu false + else -> { + val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order) + ?: return@showPopupMenu false + viewModel.setCategoryOrder(category.id, order) + } } true } @@ -125,11 +133,26 @@ class FavouritesContainerFragment : BaseFragment(), private fun wrapCategories(categories: List): List { val data = ArrayList(categories.size + 1) - data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date()) + data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, SortOrder.NEWEST, Date()) data += categories return data } + private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { + val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return + for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { + val menuItem = submenu.add( + R.id.group_order, + Menu.NONE, + i, + item.titleRes + ) + menuItem.isCheckable = true + menuItem.isChecked = item == category.order + } + submenu.setGroupCheckable(R.id.group_order, true, true) + } + companion object { fun newInstance() = FavouritesContainerFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt index 83368d802..29de80809 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesPagerAdapter.kt @@ -36,7 +36,7 @@ class FavouritesPagerAdapter( override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { val item = differ.currentList[position] tab.text = item.title - tab.view.tag = item + tab.view.tag = item.id tab.view.setOnLongClickListener(this) } @@ -45,7 +45,8 @@ class FavouritesPagerAdapter( } override fun onLongClick(v: View): Boolean { - val item = v.tag as? FavouriteCategory ?: return false + val itemId = v.tag as? Long ?: return false + val item = differ.currentList.find { x -> x.id == itemId } ?: return false return longClickListener.onTabLongClick(v, item) } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt index 6b4217f77..17639f06f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesActivity.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.os.Bundle +import android.view.Menu import android.view.View import android.view.ViewGroup import androidx.core.graphics.Insets @@ -20,6 +21,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.showPopupMenu @@ -61,10 +63,17 @@ class CategoriesActivity : BaseActivity(), } override fun onItemClick(item: FavouriteCategory, view: View) { - view.showPopupMenu(R.menu.popup_category) { + view.showPopupMenu(R.menu.popup_category, { menu -> + createOrderSubmenu(menu, item) + }) { when (it.itemId) { R.id.action_remove -> editDelegate.deleteCategory(item) R.id.action_rename -> editDelegate.renameCategory(item) + R.id.action_order -> return@showPopupMenu false + else -> { + val order = SORT_ORDERS.getOrNull(it.order) ?: return@showPopupMenu false + viewModel.setCategoryOrder(item.id, order) + } } true } @@ -117,6 +126,21 @@ class CategoriesActivity : BaseActivity(), viewModel.createCategory(name) } + private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { + val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return + for ((i, item) in SORT_ORDERS.withIndex()) { + val menuItem = submenu.add( + R.id.group_order, + Menu.NONE, + i, + item.titleRes + ) + menuItem.isCheckable = true + menuItem.isChecked = item == category.order + } + submenu.setGroupCheckable(R.id.group_order, true, true) + } + private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0 ) { @@ -145,6 +169,12 @@ class CategoriesActivity : BaseActivity(), companion object { + val SORT_ORDERS = arrayOf( + SortOrder.ALPHABETICAL, + SortOrder.NEWEST, + SortOrder.RATING, + ) + fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt index 5bc88aa05..adf19ca9c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/CategoriesAdapter.kt @@ -6,7 +6,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.FavouriteCategory class CategoriesAdapter( - onItemClickListener: OnListItemClickListener + onItemClickListener: OnListItemClickListener, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -20,12 +20,27 @@ class CategoriesAdapter( private class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean { + override fun areItemsTheSame( + oldItem: FavouriteCategory, + newItem: FavouriteCategory, + ): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean { + override fun areContentsTheSame( + oldItem: FavouriteCategory, + newItem: FavouriteCategory, + ): Boolean { return oldItem.id == newItem.id && oldItem.title == newItem.title + && oldItem.order == newItem.order + } + + override fun getChangePayload( + oldItem: FavouriteCategory, + newItem: FavouriteCategory, + ): Any? = when { + oldItem.title == newItem.title && oldItem.order != newItem.order -> newItem.order + else -> super.getChangePayload(oldItem, newItem) } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 8c648a47b..a9202d749 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -4,10 +4,10 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import java.util.* -import kotlin.collections.ArrayList class FavouritesCategoriesViewModel( private val repository: FavouritesRepository @@ -19,23 +19,29 @@ class FavouritesCategoriesViewModel( .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) fun createCategory(name: String) { - launchJob(Dispatchers.Default) { + launchJob { repository.addCategory(name) } } fun renameCategory(id: Long, name: String) { - launchJob(Dispatchers.Default) { + launchJob { repository.renameCategory(id, name) } } fun deleteCategory(id: Long) { - launchJob(Dispatchers.Default) { + launchJob { repository.removeCategory(id) } } + fun setCategoryOrder(id: Long, order: SortOrder) { + launchJob { + repository.setCategoryOrder(id, order) + } + } + fun reorderCategories(oldPos: Int, newPos: Int) { val prevJob = reorderJob reorderJob = launchJob(Dispatchers.Default) { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index ec13864b9..344c3d5a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -22,7 +23,11 @@ class FavouritesListViewModel( ) : MangaListViewModel(settings) { override val content = combine( - if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId), + if (categoryId == 0L) { + repository.observeAll(SortOrder.NEWEST) + } else { + repository.observeAll(categoryId) + }, createListModeFlow() ) { list, mode -> when { diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt index 793d0d7c3..16d12ebbd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.retry +import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.utils.ext.processLifecycleScope @@ -17,7 +18,7 @@ import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider class WidgetUpdater(private val context: Context) { fun subscribeToFavourites(repository: FavouritesRepository) { - repository.observeAll() + repository.observeAll(SortOrder.NEWEST) .onEach { updateWidget(ShelfWidgetProvider::class.java) } .retry { error -> error !is CancellationException } .launchIn(processLifecycleScope) diff --git a/app/src/main/res/menu/popup_category.xml b/app/src/main/res/menu/popup_category.xml index 92dd0a0e9..1c4fc96d2 100644 --- a/app/src/main/res/menu/popup_category.xml +++ b/app/src/main/res/menu/popup_category.xml @@ -10,4 +10,17 @@ android:id="@+id/action_rename" android:title="@string/rename" /> + + + + + + + + + \ No newline at end of file From 6037c66a2d7de9613f3106ef6d6f2fb979023ce9 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Wed, 28 Jul 2021 15:29:09 +0000 Subject: [PATCH 043/180] Translated using Weblate (Russian) Currently translated at 99.5% (224 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ --- app/src/main/res/values-ru/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 35cfc2e73..c610d5a2c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -6,7 +6,7 @@ Избранное История Произошла ошибка - Ошибка сети + Ошибка сетевого подключения Подробности Главы Список @@ -65,7 +65,7 @@ Импорт Удалить Операция не поддерживается - Поддерживаются только файлы ZIP и CBZ. + Неверный файл. Поддерживаются только ZIP и CBZ. Нет описания История и кэш Очистить кэш страниц @@ -221,5 +221,5 @@ В очереди На данный момент нет активных загрузок Глава отсутствует - Эта глава отсутствует на вашем устройстве. Скачайте её или прочитайте онлайн + Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн. \ No newline at end of file From 56f9cc2c884c19f4d9b9bebf304257bebfd33651 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Wed, 28 Jul 2021 05:48:49 +0000 Subject: [PATCH 044/180] Translated using Weblate (German) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/ --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b4bf7a2a0..cb2242e5f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -222,7 +222,7 @@ Domäne Nur Gesten Kapitel fehlt - Dieses Kapitel fehlt auf deinem Gerät. Lade ihn herunter oder lese ihn online + Dieses Kapitel fehlt auf deinem Gerät. Lade ihn herunter oder lese ihn online. Zurzeit sind keine aktiven Datenübertragungen vorhanden In Warteschlange \ No newline at end of file From e4da0a126c35ce9ab8cd4fdb824c5be75f624d95 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Wed, 28 Jul 2021 05:48:59 +0000 Subject: [PATCH 045/180] Translated using Weblate (Italian) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ --- app/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 22fb8ce94..6f866072d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -222,7 +222,7 @@ Adatta alla larghezza Modalità scala Capitolo mancante - Questo capitolo manca sul tuo dispositivo. Scaricalo o leggilo in linea + Questo capitolo manca sul tuo dispositivo. Scaricalo o leggilo in linea. Attualmente non ci sono scaricamenti attivi In coda \ No newline at end of file From 89b915b2067782ad65ed06fb9487c4044f09c918 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Wed, 28 Jul 2021 05:48:33 +0000 Subject: [PATCH 046/180] Translated using Weblate (French) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2777aac39..9a7214c04 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -222,7 +222,7 @@ Ouvrir le menu Fermer le menu Chapitre manquant - Ce chapitre est manquant sur votre appareil. Téléchargez-le ou lisez-le en ligne + Ce chapitre est manquant sur votre appareil. Téléchargez-le ou lisez-le en ligne. Il n\'y a actuellement aucun téléchargement actif En file d\'attente \ No newline at end of file From 95708367a16323985ef0cab85c9de500a3971615 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 31 Jul 2021 15:27:18 +0000 Subject: [PATCH 047/180] Translated using Weblate (Belarusian) Currently translated at 100.0% (225 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ --- app/src/main/res/values-be/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index d68b19691..92d33887b 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -6,7 +6,7 @@ Абраныя Гісторыя Адбылася памылка - Памылка сеткі + Памылка сеткавага падключэння Падрабязнасцi Часткi Спіс @@ -65,7 +65,7 @@ Імпарт Выдаліць Аперацыя не падтрымліваецца - Падтрымліваюцца толькі ZIP файлы і CBZ. + Непадтрымоўваны файл. Падтрымліваюцца толькі ZIP і CBZ. Няма апісання Гісторыя і кэш Ачысціць кэш старонак @@ -220,7 +220,7 @@ Паспрабуйце перафармуляваць запыт. Тут неяк пуста… Частка адсутнічае - Гэтая частка адсутнічае на вашым прыладзе. Загрузіце яе або прачытайце онлайн + Гэтая частка адсутнічае на вашым прыладзе. Загрузіце або прачытайце яе онлайн. На дадзены момант няма актыўных загрузак У чарзе \ No newline at end of file From cc28d4fe5445ff49822e826769d0340b83c77bd3 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sat, 31 Jul 2021 15:24:20 +0000 Subject: [PATCH 048/180] Translated using Weblate (Russian) Currently translated at 99.5% (224 of 225 strings) Translation: Kotatsu/Strings Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c610d5a2c..8fef0d91d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -65,7 +65,7 @@ Импорт Удалить Операция не поддерживается - Неверный файл. Поддерживаются только ZIP и CBZ. + Неподдерживаемый файл. Поддерживаются только ZIP и CBZ. Нет описания История и кэш Очистить кэш страниц @@ -174,7 +174,7 @@ Чёрная тёмная тема Полезно для AMOLED экранов Требуется перезапуск - Резервное копирование + Резервное копирование и восстановление Создать резервную копию Восстановить данные Данные восстановлены From 594c359f1cc25d8a03dded22963a9c40880ea21c Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 2 Aug 2021 22:44:58 +0300 Subject: [PATCH 049/180] Added information about the app to a separate activity --- app/src/main/AndroidManifest.xml | 1 + .../koitharu/kotatsu/about/AboutActivity.kt | 45 +++++++++++ .../koitharu/kotatsu/about/AboutFragment.kt | 74 +++++++++++++++++++ .../kotatsu/core/prefs/AppSettings.kt | 5 ++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 4 + .../kotatsu/settings/MainSettingsFragment.kt | 31 -------- app/src/main/res/layout/activity_about.xml | 30 ++++++++ app/src/main/res/menu/nav_drawer.xml | 4 + app/src/main/res/values-ru/strings.xml | 5 ++ app/src/main/res/values/strings.xml | 5 ++ app/src/main/res/xml/pref_about.xml | 47 ++++++++++++ app/src/main/res/xml/pref_main.xml | 29 +------- 12 files changed, 224 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/about/AboutActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt create mode 100644 app/src/main/res/layout/activity_about.xml create mode 100644 app/src/main/res/xml/pref_about.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e221bce1..3a8a5ce86 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,6 +87,7 @@ + () { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityAboutBinding.inflate(layoutInflater)) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setTitle(R.string.about) + } + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.toolbar.updatePadding( + top = insets.top, + left = insets.left, + right = insets.right + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + companion object { + + fun newIntent(context: Context) = Intent(context, AboutActivity::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt b/app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt new file mode 100644 index 000000000..02c2d3738 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt @@ -0,0 +1,74 @@ +package org.koitharu.kotatsu.about + +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.browser.BrowserActivity +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.settings.AppUpdateChecker +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope + +class AboutFragment : BasePreferenceFragment(R.string.about) { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_about) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run { + isVisible = AppUpdateChecker.isUpdateSupported(context) + } + findPreference(AppSettings.KEY_APP_VERSION)?.run { + title = getString(R.string.app_version, BuildConfig.VERSION_NAME) + isEnabled = AppUpdateChecker.isUpdateSupported(context) + } + } + + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + return when (preference?.key) { + AppSettings.KEY_APP_VERSION -> { + checkForUpdates() + true + } + AppSettings.KEY_APP_TRANSLATION -> { + startActivity(context?.let { BrowserActivity.newIntent(it, "https://hosted.weblate.org/engage/kotatsu", resources.getString(R.string.about_app_translation)) }) + true + } + AppSettings.KEY_FEEDBACK_4PDA -> { + startActivity(context?.let { BrowserActivity.newIntent(it, "https://4pda.to/forum/index.php?showtopic=697669", resources.getString(R.string.about_feedback_4pda)) }) + true + } + AppSettings.KEY_FEEDBACK_GITHUB -> { + startActivity(context?.let { BrowserActivity.newIntent(it, "https://github.com/nv95/Kotatsu/issues", "GitHub") }) + true + } + else -> super.onPreferenceTreeClick(preference) + } + } + + private fun checkForUpdates() { + viewLifecycleScope.launch { + findPreference(AppSettings.KEY_APP_VERSION)?.run { + setSummary(R.string.checking_for_updates) + isSelectable = false + } + val result = AppUpdateChecker(activity ?: return@launch).checkNow() + findPreference(AppSettings.KEY_APP_VERSION)?.run { + setSummary( + when (result) { + true -> R.string.check_for_updates + false -> R.string.no_update_available + null -> R.string.update_check_failed + } + ) + isSelectable = true + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 370f69720..7f67ebcd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -184,5 +184,10 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : const val KEY_RESTORE = "restore" const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_REVERSE_CHAPTERS = "reverse_chapters" + + // About + const val KEY_APP_TRANSLATION = "about_app_translation" + const val KEY_FEEDBACK_4PDA = "about_feedback_4pda" + const val KEY_FEEDBACK_GITHUB = "about_feedback_github" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 2788a6ee0..6c8e88429 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -25,6 +25,7 @@ import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.about.AboutActivity import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource @@ -195,6 +196,9 @@ class MainActivity : BaseActivity(), startActivity(SettingsActivity.newIntent(this)) return true } + R.id.nav_action_about -> { + startActivity(AboutActivity.newIntent(this)) + } else -> return false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt index 388059e99..a61eaa361 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/MainSettingsFragment.kt @@ -44,19 +44,12 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run { - isVisible = AppUpdateChecker.isUpdateSupported(context) - } findPreference(AppSettings.KEY_LOCAL_STORAGE)?.run { summary = settings.getStorageDir(context)?.getStorageName(context) ?: getString(R.string.not_available) } findPreference(AppSettings.KEY_PROTECT_APP)?.isChecked = !settings.appPassword.isNullOrEmpty() - findPreference(AppSettings.KEY_APP_VERSION)?.run { - title = getString(R.string.app_version, BuildConfig.VERSION_NAME) - isEnabled = AppUpdateChecker.isUpdateSupported(context) - } settings.subscribe(this) } @@ -120,10 +113,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), } true } - AppSettings.KEY_APP_VERSION -> { - checkForUpdates() - true - } else -> super.onPreferenceTreeClick(preference) } } @@ -181,24 +170,4 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), .create() .show() } - - private fun checkForUpdates() { - viewLifecycleScope.launch { - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary(R.string.checking_for_updates) - isSelectable = false - } - val result = AppUpdateChecker(activity ?: return@launch).checkNow() - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary( - when (result) { - true -> R.string.check_for_updates - false -> R.string.no_update_available - null -> R.string.update_check_failed - } - ) - isSelectable = true - } - } - } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 000000000..80d17b028 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml index 75869c7a7..f55898559 100644 --- a/app/src/main/res/menu/nav_drawer.xml +++ b/app/src/main/res/menu/nav_drawer.xml @@ -37,5 +37,9 @@ android:id="@+id/nav_action_settings" android:icon="@drawable/ic_settings" android:title="@string/settings" /> + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 35cfc2e73..fb6c6d6fe 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -222,4 +222,9 @@ На данный момент нет активных загрузок Глава отсутствует Эта глава отсутствует на вашем устройстве. Скачайте её или прочитайте онлайн + Помочь с переводом приложения + Перевод + Автор + Тема на 4PDA + Обратная связь \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c354bd6c..b2b753608 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,4 +225,9 @@ There are currently no active downloads This chapter is missing on your device. Download or read it online. Chapter is missing + Translate this app + Translation + Author + Feedback + Topic on 4PDA \ No newline at end of file diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml new file mode 100644 index 000000000..cd7919264 --- /dev/null +++ b/app/src/main/res/xml/pref_about.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index a8613f904..0a5948c24 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -79,30 +79,9 @@ android:title="@string/new_chapters_checking" app:iconSpaceReserved="false" /> - - - - - - - - - + \ No newline at end of file From 3a442817ce8b286e82de659e270a430f13e0113a Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Tue, 3 Aug 2021 18:10:09 +0300 Subject: [PATCH 050/180] Add about section to settings, add some info stuff --- app/src/main/AndroidManifest.xml | 1 - .../koitharu/kotatsu/about/AboutActivity.kt | 45 ------------------- .../kotatsu/core/prefs/AppSettings.kt | 5 ++- .../koitharu/kotatsu/main/ui/MainActivity.kt | 4 -- .../about/AboutSettingsFragment.kt} | 22 ++++++--- .../settings/about/CopyrightFragment.kt | 42 +++++++++++++++++ .../settings/about/GratitudesFragment.kt | 42 +++++++++++++++++ app/src/main/res/drawable/ic_copyright.xml | 10 +++++ .../main/res/layout/fragment_copyright.xml | 15 +++++++ .../main/res/layout/fragment_gratitudes.xml | 15 +++++++ app/src/main/res/menu/nav_drawer.xml | 4 -- app/src/main/res/raw-ru/gratitudes | 5 +++ app/src/main/res/raw/copyright | 24 ++++++++++ app/src/main/res/raw/gratitudes | 5 +++ app/src/main/res/values-ru/strings.xml | 6 +++ app/src/main/res/values/strings.xml | 6 +++ app/src/main/res/xml/pref_about.xml | 32 ++++++++++++- app/src/main/res/xml/pref_main.xml | 5 +++ 18 files changed, 226 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/about/AboutActivity.kt rename app/src/main/java/org/koitharu/kotatsu/{about/AboutFragment.kt => settings/about/AboutSettingsFragment.kt} (69%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt create mode 100644 app/src/main/res/drawable/ic_copyright.xml create mode 100644 app/src/main/res/layout/fragment_copyright.xml create mode 100644 app/src/main/res/layout/fragment_gratitudes.xml create mode 100644 app/src/main/res/raw-ru/gratitudes create mode 100644 app/src/main/res/raw/copyright create mode 100644 app/src/main/res/raw/gratitudes diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3a8a5ce86..7e221bce1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -87,7 +87,6 @@ - () { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityAboutBinding.inflate(layoutInflater)) - supportActionBar?.apply { - setDisplayHomeAsUpEnabled(true) - setTitle(R.string.about) - } - } - - override fun onWindowInsetsChanged(insets: Insets) { - binding.toolbar.updatePadding( - top = insets.top, - left = insets.left, - right = insets.right - ) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - - companion object { - - fun newIntent(context: Context) = Intent(context, AboutActivity::class.java) - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 7f67ebcd5..057e7913c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -167,8 +167,6 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : const val KEY_READER_SWITCHERS = "reader_switchers" const val KEY_TRACK_SOURCES = "track_sources" const val KEY_TRACK_WARNING = "track_warning" - const val KEY_APP_UPDATE = "app_update" - const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications" const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings" const val KEY_NOTIFICATIONS_SOUND = "notifications_sound" @@ -186,8 +184,11 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : const val KEY_REVERSE_CHAPTERS = "reverse_chapters" // About + const val KEY_APP_UPDATE = "app_update" + const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" const val KEY_FEEDBACK_4PDA = "about_feedback_4pda" const val KEY_FEEDBACK_GITHUB = "about_feedback_github" + const val KEY_SUPPORT_DEVELOPER = "about_support_developer" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 6c8e88429..2788a6ee0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -25,7 +25,6 @@ import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.about.AboutActivity import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource @@ -196,9 +195,6 @@ class MainActivity : BaseActivity(), startActivity(SettingsActivity.newIntent(this)) return true } - R.id.nav_action_about -> { - startActivity(AboutActivity.newIntent(this)) - } else -> return false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt rename to app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index 02c2d3738..193a7d282 100644 --- a/app/src/main/java/org/koitharu/kotatsu/about/AboutFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.about +package org.koitharu.kotatsu.settings.about import android.os.Bundle import android.view.View @@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.utils.ext.viewLifecycleScope -class AboutFragment : BasePreferenceFragment(R.string.about) { +class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_about) @@ -37,15 +37,27 @@ class AboutFragment : BasePreferenceFragment(R.string.about) { true } AppSettings.KEY_APP_TRANSLATION -> { - startActivity(context?.let { BrowserActivity.newIntent(it, "https://hosted.weblate.org/engage/kotatsu", resources.getString(R.string.about_app_translation)) }) + startActivity(context?.let { BrowserActivity.newIntent(it, + "https://hosted.weblate.org/engage/kotatsu", + resources.getString(R.string.about_app_translation)) }) true } AppSettings.KEY_FEEDBACK_4PDA -> { - startActivity(context?.let { BrowserActivity.newIntent(it, "https://4pda.to/forum/index.php?showtopic=697669", resources.getString(R.string.about_feedback_4pda)) }) + startActivity(context?.let { BrowserActivity.newIntent(it, + "https://4pda.to/forum/index.php?showtopic=697669", + resources.getString(R.string.about_feedback_4pda)) }) true } AppSettings.KEY_FEEDBACK_GITHUB -> { - startActivity(context?.let { BrowserActivity.newIntent(it, "https://github.com/nv95/Kotatsu/issues", "GitHub") }) + startActivity(context?.let { BrowserActivity.newIntent(it, + "https://4pda.to/forum/index.php?showtopic=697669", + resources.getString(R.string.about_feedback_4pda)) }) + true + } + AppSettings.KEY_SUPPORT_DEVELOPER -> { + startActivity(context?.let { BrowserActivity.newIntent(it, + "https://4pda.to/forum/index.php?showtopic=697669", + resources.getString(R.string.about_support_developer)) }) true } else -> super.onPreferenceTreeClick(preference) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt new file mode 100644 index 000000000..3f134492d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.settings.about + +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.text.HtmlCompat +import androidx.core.text.parseAsHtml +import androidx.core.view.updatePadding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.databinding.FragmentCopyrightBinding + +class CopyrightFragment : BaseFragment() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.textView.apply { + text = + SpannableStringBuilder(resources.openRawResource(R.raw.copyright).bufferedReader() + .readText() + .parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST)) + movementMethod = LinkMovementMethod.getInstance() + } + } + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup? + ) = FragmentCopyrightBinding.inflate(inflater, container, false) + + override fun onResume() { + super.onResume() + activity?.setTitle(R.string.about_copyright) + } + + override fun onWindowInsetsChanged(insets: Insets) = Unit + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt new file mode 100644 index 000000000..3a1a0ddd8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.settings.about + +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.graphics.Insets +import androidx.core.text.HtmlCompat +import androidx.core.text.parseAsHtml +import androidx.core.view.updatePadding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.databinding.FragmentGratitudesBinding + +class GratitudesFragment : BaseFragment() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.textView.apply { + text = + SpannableStringBuilder(resources.openRawResource(R.raw.gratitudes).bufferedReader() + .readText() + .parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST)) + movementMethod = LinkMovementMethod.getInstance() + } + } + + override fun onInflateView( + inflater: LayoutInflater, + container: ViewGroup? + ) = FragmentGratitudesBinding.inflate(inflater, container, false) + + override fun onResume() { + super.onResume() + activity?.setTitle(R.string.about_gratitudes) + } + + override fun onWindowInsetsChanged(insets: Insets) = Unit + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_copyright.xml b/app/src/main/res/drawable/ic_copyright.xml new file mode 100644 index 000000000..7cb1adcdc --- /dev/null +++ b/app/src/main/res/drawable/ic_copyright.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_copyright.xml b/app/src/main/res/layout/fragment_copyright.xml new file mode 100644 index 000000000..d77f49e1d --- /dev/null +++ b/app/src/main/res/layout/fragment_copyright.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_gratitudes.xml b/app/src/main/res/layout/fragment_gratitudes.xml new file mode 100644 index 000000000..d77f49e1d --- /dev/null +++ b/app/src/main/res/layout/fragment_gratitudes.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml index f55898559..75869c7a7 100644 --- a/app/src/main/res/menu/nav_drawer.xml +++ b/app/src/main/res/menu/nav_drawer.xml @@ -37,9 +37,5 @@ android:id="@+id/nav_action_settings" android:icon="@drawable/ic_settings" android:title="@string/settings" /> - \ No newline at end of file diff --git a/app/src/main/res/raw-ru/gratitudes b/app/src/main/res/raw-ru/gratitudes new file mode 100644 index 000000000..4d74c4cac --- /dev/null +++ b/app/src/main/res/raw-ru/gratitudes @@ -0,0 +1,5 @@ +Благодарности:
+

Zakhar Timoshenko (Xtimms) - активная помощь в разработке в плане пользовательского интерфейса и перевод на белорусский язык

+

Allan Nordhøy (comradekingu) - перевод на норвежский букмол

+

sguinetti - перевод на испанский

+

J. Lavoie - перевод на французский, итальянский и немецкий

\ No newline at end of file diff --git a/app/src/main/res/raw/copyright b/app/src/main/res/raw/copyright new file mode 100644 index 000000000..bf4212730 --- /dev/null +++ b/app/src/main/res/raw/copyright @@ -0,0 +1,24 @@ +

Kotatsu is a free and open source manga reader for Android.

+

Copyright (C) 2020 by Koitharu

+

This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version.

+

This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details.

+

You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/.

+

Open Source Licenses

+ \ No newline at end of file diff --git a/app/src/main/res/raw/gratitudes b/app/src/main/res/raw/gratitudes new file mode 100644 index 000000000..fbf8de215 --- /dev/null +++ b/app/src/main/res/raw/gratitudes @@ -0,0 +1,5 @@ +Thanks:
+

Zakhar Timoshenko (Xtimms) - active assistance in the development from the point of view of the UI and translation into the Belarusian language

+

Allan Nordhøy (comradekingu) - Norwegian Bokmål translation

+

sguinetti - Spanish translation

+

J. Lavoie - French, German and Italian translation

\ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fb6c6d6fe..8a2e65b13 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -227,4 +227,10 @@ Автор Тема на 4PDA Обратная связь + Поддержать разработчика + Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) + Благодарности + Эти люди помогают Kotatsu стать лучше! + Авторские права и лицензии + Авторские права \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2b753608..0f844b714 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,4 +230,10 @@ Author Feedback Topic on 4PDA + Support the developer + If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) + Gratitudes + These people make Kotatsu become better! + Copyright & Licenses + Copyright \ No newline at end of file diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index cd7919264..dcc47a0cc 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -1,7 +1,9 @@ + xmlns:tools="http://schemas.android.com/tools" + app:initialExpandedChildrenCount="5"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_main.xml b/app/src/main/res/xml/pref_main.xml index 0a5948c24..7f9d8bf84 100644 --- a/app/src/main/res/xml/pref_main.xml +++ b/app/src/main/res/xml/pref_main.xml @@ -84,4 +84,9 @@ android:title="@string/backup_restore" app:iconSpaceReserved="false" /> + + \ No newline at end of file From 253f4abba15d2603d422db9a5498c791fd4607b6 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Tue, 3 Aug 2021 18:17:31 +0300 Subject: [PATCH 051/180] Minor fix --- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8a2e65b13..cd1c8d28f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -227,7 +227,7 @@ Автор Тема на 4PDA Обратная связь - Поддержать разработчика + Поддержать разработчика Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) Благодарности Эти люди помогают Kotatsu стать лучше! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f844b714..a8bd88197 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,10 +230,10 @@ Author Feedback Topic on 4PDA - Support the developer + Support the developer If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) Gratitudes These people make Kotatsu become better! - Copyright & Licenses + Copyright & Licenses Copyright \ No newline at end of file From eb5976a7965201335fce6fae5f66f99dcada8cd0 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Tue, 3 Aug 2021 20:11:18 +0300 Subject: [PATCH 052/180] Some changes in about section, fix links --- .../kotatsu/core/prefs/AppSettings.kt | 1 + .../settings/about/AboutSettingsFragment.kt | 12 ++++-- .../settings/about/GratitudesFragment.kt | 42 ------------------- ...opyrightFragment.kt => LicenseFragment.kt} | 5 +-- app/src/main/res/drawable/ic_copyleft.xml | 16 +++++++ app/src/main/res/drawable/ic_copyright.xml | 10 ----- app/src/main/res/raw-ru/gratitudes | 5 --- app/src/main/res/raw/gratitudes | 5 --- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/pref_about.xml | 9 ++-- 11 files changed, 34 insertions(+), 75 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt rename app/src/main/java/org/koitharu/kotatsu/settings/about/{CopyrightFragment.kt => LicenseFragment.kt} (88%) create mode 100644 app/src/main/res/drawable/ic_copyleft.xml delete mode 100644 app/src/main/res/drawable/ic_copyright.xml delete mode 100644 app/src/main/res/raw-ru/gratitudes delete mode 100644 app/src/main/res/raw/gratitudes diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 057e7913c..a2f2fbc59 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -187,6 +187,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) : const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" + const val KEY_APP_GRATITUDES = "about_gratitudes" const val KEY_FEEDBACK_4PDA = "about_feedback_4pda" const val KEY_FEEDBACK_GITHUB = "about_feedback_github" const val KEY_SUPPORT_DEVELOPER = "about_support_developer" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index 193a7d282..19107be54 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -50,16 +50,22 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { } AppSettings.KEY_FEEDBACK_GITHUB -> { startActivity(context?.let { BrowserActivity.newIntent(it, - "https://4pda.to/forum/index.php?showtopic=697669", - resources.getString(R.string.about_feedback_4pda)) }) + "https://github.com/nv95/Kotatsu/issues", + "GitHub") }) true } AppSettings.KEY_SUPPORT_DEVELOPER -> { startActivity(context?.let { BrowserActivity.newIntent(it, - "https://4pda.to/forum/index.php?showtopic=697669", + "https://yoomoney.ru/to/410012543938752", resources.getString(R.string.about_support_developer)) }) true } + AppSettings.KEY_APP_GRATITUDES -> { + startActivity(context?.let { BrowserActivity.newIntent(it, + "https://github.com/nv95/Kotatsu/graphs/contributors", + resources.getString(R.string.about_gratitudes)) }) + true + } else -> super.onPreferenceTreeClick(preference) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt deleted file mode 100644 index 3a1a0ddd8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/GratitudesFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.koitharu.kotatsu.settings.about - -import android.os.Bundle -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.graphics.Insets -import androidx.core.text.HtmlCompat -import androidx.core.text.parseAsHtml -import androidx.core.view.updatePadding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseFragment -import org.koitharu.kotatsu.databinding.FragmentGratitudesBinding - -class GratitudesFragment : BaseFragment() { - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.textView.apply { - text = - SpannableStringBuilder(resources.openRawResource(R.raw.gratitudes).bufferedReader() - .readText() - .parseAsHtml(HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST)) - movementMethod = LinkMovementMethod.getInstance() - } - } - - override fun onInflateView( - inflater: LayoutInflater, - container: ViewGroup? - ) = FragmentGratitudesBinding.inflate(inflater, container, false) - - override fun onResume() { - super.onResume() - activity?.setTitle(R.string.about_gratitudes) - } - - override fun onWindowInsetsChanged(insets: Insets) = Unit - -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt similarity index 88% rename from app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt rename to app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt index 3f134492d..8d25e3ca9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/CopyrightFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/LicenseFragment.kt @@ -9,12 +9,11 @@ import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml -import androidx.core.view.updatePadding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.databinding.FragmentCopyrightBinding -class CopyrightFragment : BaseFragment() { +class LicenseFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -34,7 +33,7 @@ class CopyrightFragment : BaseFragment() { override fun onResume() { super.onResume() - activity?.setTitle(R.string.about_copyright) + activity?.setTitle(R.string.about_license) } override fun onWindowInsetsChanged(insets: Insets) = Unit diff --git a/app/src/main/res/drawable/ic_copyleft.xml b/app/src/main/res/drawable/ic_copyleft.xml new file mode 100644 index 000000000..0227f2d77 --- /dev/null +++ b/app/src/main/res/drawable/ic_copyleft.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_copyright.xml b/app/src/main/res/drawable/ic_copyright.xml deleted file mode 100644 index 7cb1adcdc..000000000 --- a/app/src/main/res/drawable/ic_copyright.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/raw-ru/gratitudes b/app/src/main/res/raw-ru/gratitudes deleted file mode 100644 index 4d74c4cac..000000000 --- a/app/src/main/res/raw-ru/gratitudes +++ /dev/null @@ -1,5 +0,0 @@ -Благодарности:
-

Zakhar Timoshenko (Xtimms) - активная помощь в разработке в плане пользовательского интерфейса и перевод на белорусский язык

-

Allan Nordhøy (comradekingu) - перевод на норвежский букмол

-

sguinetti - перевод на испанский

-

J. Lavoie - перевод на французский, итальянский и немецкий

\ No newline at end of file diff --git a/app/src/main/res/raw/gratitudes b/app/src/main/res/raw/gratitudes deleted file mode 100644 index fbf8de215..000000000 --- a/app/src/main/res/raw/gratitudes +++ /dev/null @@ -1,5 +0,0 @@ -Thanks:
-

Zakhar Timoshenko (Xtimms) - active assistance in the development from the point of view of the UI and translation into the Belarusian language

-

Allan Nordhøy (comradekingu) - Norwegian Bokmål translation

-

sguinetti - Spanish translation

-

J. Lavoie - French, German and Italian translation

\ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cd1c8d28f..8ffe11b84 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -232,5 +232,5 @@ Благодарности Эти люди помогают Kotatsu стать лучше! Авторские права и лицензии - Авторские права + Лицензия \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8bd88197..6566b5749 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,5 +235,5 @@ Gratitudes These people make Kotatsu become better! Copyright & Licenses - Copyright + License \ No newline at end of file diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index dcc47a0cc..fd82426ce 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -37,7 +37,6 @@ app:title="@string/about_support_developer" /> + app:key="about_license" + app:title="@string/about_license" /> From d6c6132a04044c646956862eab658013ea5dbbfb Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Tue, 3 Aug 2021 20:14:38 +0300 Subject: [PATCH 053/180] Fixes --- app/src/main/res/layout/activity_about.xml | 30 ------------------- .../main/res/layout/fragment_copyright.xml | 6 ++-- .../main/res/layout/fragment_gratitudes.xml | 15 ---------- 3 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 app/src/main/res/layout/activity_about.xml delete mode 100644 app/src/main/res/layout/fragment_gratitudes.xml diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index 80d17b028..000000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_copyright.xml b/app/src/main/res/layout/fragment_copyright.xml index d77f49e1d..fe9c3e4e1 100644 --- a/app/src/main/res/layout/fragment_copyright.xml +++ b/app/src/main/res/layout/fragment_copyright.xml @@ -1,15 +1,15 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_gratitudes.xml b/app/src/main/res/layout/fragment_gratitudes.xml deleted file mode 100644 index d77f49e1d..000000000 --- a/app/src/main/res/layout/fragment_gratitudes.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file From 57111f628de1a824c306d005e7b790fa73547b52 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Thu, 29 Jul 2021 22:35:04 +0300 Subject: [PATCH 054/180] Minor tweaks --- .../kotatsu/base/ui/widgets/ChipsView.kt | 2 +- .../koitharu/kotatsu/main/ui/MainActivity.kt | 6 +++--- app/src/main/res/layout/item_empty_state.xml | 19 +++++++---------- app/src/main/res/values/dimens.xml | 21 +++---------------- app/src/main/res/values/styles.xml | 21 +++++++++++++++++-- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt index f8a839efc..84a34261e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt @@ -68,7 +68,7 @@ class ChipsView @JvmOverloads constructor( val chip = Chip(context) val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip) chip.setChipDrawable(drawable) - chip.setTextColor(ContextCompat.getColor(context, R.color.blue_primary)) + chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary)) chip.isCloseIconVisible = false chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 2788a6ee0..95437d333 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -204,9 +204,9 @@ class MainActivity : BaseActivity(), override fun onWindowInsetsChanged(insets: Insets) { binding.toolbarCard.updateLayoutParams { - topMargin = insets.top + 16 - leftMargin = insets.left + 32 - rightMargin = insets.right + 32 + topMargin = insets.top + resources.resolveDp(8) + leftMargin = insets.left + resources.resolveDp(16) + rightMargin = insets.right + resources.resolveDp(16) } binding.fab.updateLayoutParams { bottomMargin = insets.bottom + topMargin diff --git a/app/src/main/res/layout/item_empty_state.xml b/app/src/main/res/layout/item_empty_state.xml index 0d7f40c65..54e7c747a 100644 --- a/app/src/main/res/layout/item_empty_state.xml +++ b/app/src/main/res/layout/item_empty_state.xml @@ -1,38 +1,35 @@ + android:gravity="center"> + android:src="@drawable/ic_alert_outline" /> + tools:text="@tools:sample/lorem[15]" /> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a08cbf9eb..c308cb1f7 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,7 +3,7 @@ 16dp - + 8dp 36dp 24dp @@ -26,22 +26,7 @@ 48dp 16dp - - - 16dp - 16sp - 48dp - 56dp - 1dp - 0dp - 48dp - 56dp - 8dp - 0dp - 3dp - 0dp - 16dp - 8dp - 0dp + + 22sp \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ce368d8a2..9d246d5e3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,6 +2,8 @@ + + + + + + + \ No newline at end of file From 7bb809f2270490c19a6ec6cbded387d7eba1db00 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Thu, 29 Jul 2021 22:37:08 +0300 Subject: [PATCH 055/180] Improve thumbnail sheet --- .../ui/thumbnails/PagesThumbnailsSheet.kt | 43 ++++++++----------- app/src/main/res/layout/sheet_pages.xml | 25 +++-------- app/src/main/res/values/styles.xml | 4 ++ 3 files changed, 28 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt index 06083d02e..69e5c275d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/PagesThumbnailsSheet.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import org.koin.android.ext.android.get @@ -53,6 +54,15 @@ class PagesThumbnailsSheet : BaseBottomSheet(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + val title = arguments?.getString(ARG_TITLE) + binding.toolbar.title = title + binding.toolbar.setNavigationOnClickListener { dismiss() } + binding.toolbar.subtitle = + resources.getQuantityString(R.plurals.pages, thumbnails.size, thumbnails.size) + + val initialTopPosition = binding.recyclerView.top + with(binding.recyclerView) { addItemDecoration( SpacingItemDecoration(view.resources.getDimensionPixelOffset(R.dimen.grid_spacing)) @@ -64,42 +74,27 @@ class PagesThumbnailsSheet : BaseBottomSheet(), get(), this@PagesThumbnailsSheet ) + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) = Unit + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + binding.appbar.isLifted = getChildAt(0).top < initialTopPosition + } + }) addOnLayoutChangeListener(spanResolver) spanResolver.setGridSize(get().gridSize / 100f, this) } - - val title = arguments?.getString(ARG_TITLE) - binding.toolbar.title = title - binding.toolbar.setNavigationOnClickListener { dismiss() } - binding.toolbar.subtitle = - resources.getQuantityString(R.plurals.pages, thumbnails.size, thumbnails.size) - binding.textViewTitle.text = title - if (dialog !is BottomSheetDialog) { - binding.toolbar.isVisible = true - binding.textViewTitle.isVisible = false - binding.appbar.elevation = resources.getDimension(R.dimen.elevation_large) - } } override fun onCreateDialog(savedInstanceState: Bundle?) = super.onCreateDialog(savedInstanceState).also { val behavior = (it as? BottomSheetDialog)?.behavior ?: return@also behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { - private val elevation = resources.getDimension(R.dimen.elevation_large) override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit - override fun onStateChanged(bottomSheet: View, newState: Int) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { - binding.toolbar.isVisible = true - binding.textViewTitle.isVisible = false - binding.appbar.elevation = elevation - } else { - binding.toolbar.isVisible = false - binding.textViewTitle.isVisible = true - binding.appbar.elevation = 0f - } - } + override fun onStateChanged(bottomSheet: View, newState: Int) = Unit }) } diff --git a/app/src/main/res/layout/sheet_pages.xml b/app/src/main/res/layout/sheet_pages.xml index 2552723d5..03faf7477 100644 --- a/app/src/main/res/layout/sheet_pages.xml +++ b/app/src/main/res/layout/sheet_pages.xml @@ -9,33 +9,17 @@ + app:liftOnScroll="true"> - - + app:navigationIcon="@drawable/ic_cross" /> @@ -47,6 +31,7 @@ android:padding="@dimen/grid_spacing" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:spanCount="3" tools:listitem="@layout/item_page_thumb" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9d246d5e3..b53cfab6e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -9,6 +9,10 @@ 8dp + + - - \ No newline at end of file From 27293f1bf8914e20273887ee0698ebea9dce9749 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 5 Sep 2021 16:00:15 +0300 Subject: [PATCH 071/180] Remove some findViewById --- .../favourites/ui/FavouritesContainerFragment.kt | 4 ++-- .../org/koitharu/kotatsu/list/ui/MangaListFragment.kt | 4 ++-- .../java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt | 8 ++++++++ .../java/org/koitharu/kotatsu/main/ui/MainActivity.kt | 10 +++++++--- .../search/ui/suggestion/SearchSuggestionFragment.kt | 6 +++--- .../org/koitharu/kotatsu/tracker/ui/FeedFragment.kt | 5 +++-- 6 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index a76612600..399314311 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -6,7 +6,6 @@ import androidx.core.graphics.Insets import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator import org.koin.androidx.viewmodel.ext.android.viewModel @@ -18,6 +17,7 @@ import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.utils.RecycledViewPoolHolder import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.measureHeight @@ -68,7 +68,7 @@ class FavouritesContainerFragment : BaseFragment(), } override fun onWindowInsetsChanged(insets: Insets) { - val headerHeight = requireActivity().findViewById(R.id.appbar).measureHeight() + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.root.updatePadding( top = headerHeight - insets.top ) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index cbb215e00..066da26ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -14,7 +14,6 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import org.koin.android.ext.android.get @@ -37,6 +36,7 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.filter.FilterAdapter import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.utils.RecycledViewPoolHolder import org.koitharu.kotatsu.utils.ext.* @@ -223,7 +223,7 @@ abstract class MangaListFragment : BaseFragment(), } override fun onWindowInsetsChanged(insets: Insets) { - val headerHeight = requireActivity().findViewById(R.id.appbar).measureHeight() + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.recyclerViewFilter.updatePadding( top = headerHeight, bottom = insets.bottom diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt new file mode 100644 index 000000000..d5a2de5bc --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/AppBarOwner.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.main.ui + +import com.google.android.material.appbar.AppBarLayout + +interface AppBarOwner { + + val appBar: AppBarLayout +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index c651f557d..4272a2b5e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.CircularProgressDrawable +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get @@ -50,7 +51,7 @@ import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.* class MainActivity : BaseActivity(), - NavigationView.OnNavigationItemSelectedListener, + NavigationView.OnNavigationItemSelectedListener, AppBarOwner, View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener { private val viewModel by viewModel(mode = LazyThreadSafetyMode.NONE) @@ -62,6 +63,9 @@ class MainActivity : BaseActivity(), private lateinit var drawerToggle: ActionBarDrawerToggle private var searchViewElevation = 0f + override val appBar: AppBarLayout + get() = binding.appbar + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater)) @@ -334,7 +338,7 @@ class MainActivity : BaseActivity(), binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawerToggle.isDrawerIndicatorEnabled = false // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/dark - binding.appbar.setBackgroundColor(resources.getColor(R.color.color_on_secondary)) + binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_on_secondary)) binding.toolbarCard.cardElevation = 0f binding.appbar.elevation = searchViewElevation } @@ -343,7 +347,7 @@ class MainActivity : BaseActivity(), binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) drawerToggle.isDrawerIndicatorEnabled = true // Returning transparent color - binding.appbar.setBackgroundColor(resources.getColor(android.R.color.transparent)) + binding.appbar.setBackgroundColor(Color.TRANSPARENT) binding.appbar.elevation = 0f binding.toolbarCard.cardElevation = searchViewElevation } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt index 3777955dd..f48e9b0da 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -7,12 +7,11 @@ import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper -import com.google.android.material.appbar.AppBarLayout import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.sharedViewModel -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter import org.koitharu.kotatsu.utils.ext.measureHeight @@ -42,8 +41,9 @@ class SearchSuggestionFragment : BaseFragment() } override fun onWindowInsetsChanged(insets: Insets) { + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.root.updatePadding( - top = requireActivity().findViewById(R.id.appbar).measureHeight(), + top = headerHeight, left = insets.left, right = insets.right, bottom = insets.bottom, diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt index b24c74b31..66413a5f5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/ui/FeedFragment.kt @@ -5,7 +5,6 @@ import android.view.* import androidx.appcompat.app.AlertDialog import androidx.core.graphics.Insets import androidx.core.view.updatePadding -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -17,6 +16,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -98,8 +98,9 @@ class FeedFragment : BaseFragment(), PaginationScrollListen } override fun onWindowInsetsChanged(insets: Insets) { + val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top binding.recyclerView.updatePadding( - top = requireActivity().findViewById(R.id.appbar).measureHeight(), + top = headerHeight, left = insets.left, right = insets.right, bottom = insets.bottom From 593624fdb996b4c9980e306f80e5caefb860e1cb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 8 Sep 2021 07:27:25 +0300 Subject: [PATCH 072/180] Manga repository authorization support --- app/src/main/AndroidManifest.xml | 20 ++- .../kotatsu/base/domain/MangaLoaderContext.kt | 17 +++ .../koitharu/kotatsu/base/ui/BaseFragment.kt | 1 + .../koitharu/kotatsu/browser/BrowserClient.kt | 17 --- .../kotatsu/core/model/MangaFilter.kt | 4 +- .../parser/MangaRepositoryAuthProvider.kt | 8 ++ .../kotatsu/core/prefs/SourceSettings.kt | 1 + .../kotatsu/list/ui/filter/FilterAdapter.kt | 3 +- .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../reader/ui/SimpleSettingsActivity.kt | 14 +++ .../remotelist/ui/RemoteListFragment.kt | 25 ++++ .../remotelist/ui/RemoteListViewModel.kt | 3 + .../kotatsu/search/ui/SearchViewModel.kt | 2 +- .../settings/HistorySettingsFragment.kt | 28 +++-- .../settings/SourceSettingsFragment.kt | 27 ++++- .../sources/auth/SourceAuthActivity.kt | 114 ++++++++++++++++++ app/src/main/res/menu/opt_list_remote.xml | 11 ++ app/src/main/res/values-ru/strings.xml | 43 ++++--- app/src/main/res/values/strings.xml | 43 ++++--- app/src/main/res/xml/pref_source.xml | 7 ++ 20 files changed, 312 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt create mode 100644 app/src/main/res/menu/opt_list_remote.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e221bce1..e18e81392 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,9 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute"> - + @@ -32,12 +34,16 @@ android:name="android.app.default_searchable" android:value=".ui.search.SearchActivity" /> - + - + @@ -50,13 +56,19 @@ android:label="@string/settings" /> - + + { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTP) + .host(domain) + .build() + return cookieJar.loadForRequest(url) + } + + fun copyCookies(oldDomain: String, newDomain: String) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTP) + .host(oldDomain) + val cookies = cookieJar.loadForRequest(url.build()) + url.host(newDomain) + cookieJar.saveFromResponse(url.build(), cookies) + } + private companion object { private const val SCHEME_HTTP = "http" diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt index 27f923937..76f774bc8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFragment.kt @@ -38,6 +38,7 @@ abstract class BaseFragment : Fragment(), OnApplyWindowInsetsLi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + lastInsets = Insets.NONE ViewCompat.setOnApplyWindowInsetsListener(view, this) } diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt index 4a666d1c7..f39da5fb5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt +++ b/app/src/main/java/org/koitharu/kotatsu/browser/BrowserClient.kt @@ -1,10 +1,8 @@ package org.koitharu.kotatsu.browser import android.graphics.Bitmap -import android.webkit.WebResourceResponse import android.webkit.WebView import okhttp3.OkHttpClient -import okhttp3.Request import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koitharu.kotatsu.core.network.WebViewClientCompat @@ -27,19 +25,4 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat super.onPageCommitVisible(view, url) callback.onTitleChanged(view.title.orEmpty(), url) } - - override fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? { - return runCatching { - val request = Request.Builder() - .url(url) - .build() - val response = okHttp.newCall(request).execute() - val ct = response.body?.contentType() - WebResourceResponse( - "${ct?.type}/${ct?.subtype}", - ct?.charset()?.name() ?: "utf-8", - response.body?.byteStream() - ) - }.getOrNull() - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt index 68a3b5174..814c00571 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt @@ -5,6 +5,6 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MangaFilter( - val sortOrder: SortOrder, - val tag: MangaTag? + val sortOrder: SortOrder?, + val tag: MangaTag?, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt new file mode 100644 index 000000000..16cc5e39f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepositoryAuthProvider.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.core.parser + +interface MangaRepositoryAuthProvider { + + val authUrl: String + + fun isAuthorized(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index 6ee3d0747..7a6036d06 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -27,5 +27,6 @@ interface SourceSettings { const val KEY_DOMAIN = "domain" const val KEY_USE_SSL = "ssl" + const val KEY_AUTH = "auth" } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index b0b32096a..e7fe78058 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -7,7 +7,6 @@ import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder import java.util.* -import kotlin.collections.ArrayList class FilterAdapter( sortOrders: List = emptyList(), @@ -19,7 +18,7 @@ class FilterAdapter( private val sortOrders = ArrayList(sortOrders) private val tags = ArrayList(Collections.singletonList(null) + tags) - private var currentState = state ?: MangaFilter(sortOrders.first(), null) + private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), null) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { VIEW_TYPE_SORT -> FilterSortHolder(parent).apply { diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 1991e2c0e..844cbcbbc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -61,7 +61,7 @@ class LocalListViewModel( launchLoadingJob(Dispatchers.Default) { try { listError.value = null - mangaList.value = repository.getList(0) + mangaList.value = repository.getList(0, tags = null) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt index 1c62b2cce..0113bb475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/SimpleSettingsActivity.kt @@ -3,16 +3,19 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.Parcelable import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.fragment.app.commit import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding import org.koitharu.kotatsu.settings.MainSettingsFragment import org.koitharu.kotatsu.settings.NetworkSettingsFragment import org.koitharu.kotatsu.settings.ReaderSettingsFragment +import org.koitharu.kotatsu.settings.SourceSettingsFragment class SimpleSettingsActivity : BaseActivity() { @@ -25,6 +28,9 @@ class SimpleSettingsActivity : BaseActivity() { R.id.container, when (intent?.action) { Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment() ACTION_READER -> ReaderSettingsFragment() + ACTION_SOURCE -> SourceSettingsFragment.newInstance( + intent.getParcelableExtra(EXTRA_SOURCE) ?: MangaSource.LOCAL + ) else -> MainSettingsFragment() } ) @@ -43,9 +49,17 @@ class SimpleSettingsActivity : BaseActivity() { private const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS" + private const val ACTION_SOURCE = + "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS" + private const val EXTRA_SOURCE = "source" fun newReaderSettingsIntent(context: Context) = Intent(context, SimpleSettingsActivity::class.java) .setAction(ACTION_READER) + + fun newSourceSettingsIntent(context: Context, source: MangaSource) = + Intent(context, SimpleSettingsActivity::class.java) + .setAction(ACTION_SOURCE) + .putExtra(EXTRA_SOURCE, source as Parcelable) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 1c5004eb4..2ecb7f7bf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -1,10 +1,15 @@ package org.koitharu.kotatsu.remotelist.ui +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.list.ui.MangaListFragment +import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity import org.koitharu.kotatsu.utils.ext.parcelableArgument import org.koitharu.kotatsu.utils.ext.withArgs @@ -29,6 +34,26 @@ class RemoteListFragment : MangaListFragment() { super.onFilterChanged(filter) } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.opt_list_remote, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_source_settings -> { + startActivity( + SimpleSettingsActivity.newSourceSettingsIntent( + context ?: return false, + source, + ) + ) + true + } + else -> super.onOptionsItemSelected(item) + } + } + companion object { private const val ARG_SOURCE = "provider" diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index b344b2bda..aaf6ccaa3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -90,6 +90,9 @@ class RemoteListViewModel( } hasNextPage.value = list.isNotEmpty() } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } listError.value = e } } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 193817c33..35df96c82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* class SearchViewModel( private val repository: MangaRepository, @@ -74,6 +73,7 @@ class SearchViewModel( listError.value = null val list = repository.getList( offset = if (append) mangaList.value?.size ?: 0 else 0, + tags = null, query = query ) if (!append) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index 8380e414f..99ec51e9c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -76,15 +76,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach true } AppSettings.KEY_COOKIES_CLEAR -> { - viewLifecycleScope.launch { - val cookieJar = get() - cookieJar.clear() - Snackbar.make( - listView ?: return@launch, - R.string.cookies_cleared, - Snackbar.LENGTH_SHORT - ).show() - } + clearCookies() true } AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { @@ -144,4 +136,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } }.show() } + + private fun clearCookies() { + AlertDialog.Builder(context ?: return) + .setTitle(R.string.clear_cookies) + .setMessage(R.string.text_clear_cookies_prompt) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.clear) { _, _ -> + viewLifecycleScope.launch { + val cookieJar = get() + cookieJar.clear() + Snackbar.make( + listView ?: return@launch, + R.string.cookies_cleared, + Snackbar.LENGTH_SHORT + ).show() + } + }.show() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 4bccaf8e2..da9115e80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -9,8 +9,10 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.TwoStatePreference import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.SourceSettings +import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.utils.EditTextBindListener import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf @@ -20,6 +22,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs class SourceSettingsFragment : PreferenceFragmentCompat() { private val source by parcelableArgument(EXTRA_SOURCE) + private var repository: RemoteMangaRepository? = null override fun onResume() { super.onResume() @@ -29,6 +32,7 @@ class SourceSettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.sharedPreferencesName = source.name val repo = mangaRepositoryOf(source) as? RemoteMangaRepository ?: return + repository = repo addPreferencesFromResource(R.xml.pref_source) val screen = preferenceScreen val prefsMap = ArrayMap(screen.preferenceCount) @@ -41,13 +45,32 @@ class SourceSettingsFragment : PreferenceFragmentCompat() { initPreferenceWithDefaultValue(pref, defValue) } } + findPreference(SourceSettings.KEY_AUTH)?.run { + isVisible = repo is MangaRepositoryAuthProvider + isEnabled = (repo as? MangaRepositoryAuthProvider)?.isAuthorized() == false + } + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + return when (preference?.key) { + SourceSettings.KEY_AUTH -> { + startActivity( + SourceAuthActivity.newIntent( + context ?: return false, + source, + ) + ) + true + } + else -> super.onPreferenceTreeClick(preference) + } } private fun initPreferenceWithDefaultValue(preference: Preference, defaultValue: Any) { - when(preference) { + when (preference) { is EditTextPreference -> { preference.summaryProvider = EditTextDefaultSummaryProvider(defaultValue.toString()) - when(preference.key) { + when (preference.key) { SourceSettings.KEY_DOMAIN -> preference.setOnBindEditTextListener( EditTextBindListener( EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt new file mode 100644 index 000000000..896c92780 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -0,0 +1,114 @@ +package org.koitharu.kotatsu.settings.sources.auth + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.MenuItem +import android.widget.Toast +import androidx.core.graphics.Insets +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.browser.BrowserCallback +import org.koitharu.kotatsu.browser.BrowserClient +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider +import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf + +class SourceAuthActivity : BaseActivity(), BrowserCallback { + + private lateinit var repository: MangaRepositoryAuthProvider + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityBrowserBinding.inflate(layoutInflater)) + val source = intent?.getParcelableExtra(EXTRA_SOURCE) + if (source == null) { + finish() + return + } + repository = mangaRepositoryOf(source) as? MangaRepositoryAuthProvider ?: run { + Toast.makeText( + this, + getString(R.string.auth_not_supported_by, source.title), + Toast.LENGTH_SHORT + ).show() + finishAfterTransition() + return + } + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_cross) + } + with(binding.webView.settings) { + javaScriptEnabled = true + } + binding.webView.webViewClient = BrowserClient(this) + val url = repository.authUrl + onTitleChanged( + source.title, + getString(R.string.loading_) + ) + binding.webView.loadUrl(url) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + binding.webView.stopLoading() + finishAfterTransition() + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (binding.webView.canGoBack()) { + binding.webView.goBack() + } else { + super.onBackPressed() + } + } + + override fun onPause() { + binding.webView.onPause() + super.onPause() + } + + override fun onResume() { + super.onResume() + binding.webView.onResume() + } + + override fun onLoadingStateChanged(isLoading: Boolean) { + binding.progressBar.isVisible = isLoading + if (!isLoading && repository.isAuthorized()) { + Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show() + finishAfterTransition() + } + } + + override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { + this.title = title + supportActionBar?.subtitle = subtitle + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.appbar.updatePadding(top = insets.top) + binding.webView.updatePadding(bottom = insets.bottom) + } + + companion object { + + private const val EXTRA_SOURCE = "source" + + fun newIntent(context: Context, source: MangaSource): Intent { + return Intent(context, SourceAuthActivity::class.java) + .putExtra(EXTRA_SOURCE, source as Parcelable) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/menu/opt_list_remote.xml b/app/src/main/res/menu/opt_list_remote.xml new file mode 100644 index 000000000..deb531840 --- /dev/null +++ b/app/src/main/res/menu/opt_list_remote.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 29428323b..35be37286 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -213,24 +213,27 @@ Другие Описание Языки - Добро пожаловать - Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. - Резервная копия успешно сохранена - Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. - Подробнее - В очереди - На данный момент нет активных загрузок - Глава отсутствует - Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн. - Помочь с переводом приложения - Перевод - Автор - Тема на 4PDA - Обратная связь - Поддержать разработчика - Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) - Благодарности - Эти люди помогают Kotatsu стать лучше! - Авторские права и лицензии - Лицензия + Добро пожаловать + Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено. + Резервная копия успешно сохранена + Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач. + Подробнее + В очереди + На данный момент нет активных загрузок + Глава отсутствует + Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн. + Помочь с переводом приложения + Перевод + Автор + Тема на 4PDA + Обратная связь + Поддержать разработчика + Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги) + Благодарности + Эти люди помогают Kotatsu стать лучше! + Авторские права и лицензии + Лицензия + Авторизация выполнена + Авторизация в %s не поддерживается + Вы выйдете из всех источников, в которых Вы авторизованы \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6566b5749..f82f4b5d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,24 +216,27 @@ Do you really want to remove all recent search queries? This action cannot be undone. Other Languages - Welcome - Description - Backup saved successfully - Some manufacturers can change the system behavior, which may breaks background tasks. - Read more - Queued - There are currently no active downloads - This chapter is missing on your device. Download or read it online. - Chapter is missing - Translate this app - Translation - Author - Feedback - Topic on 4PDA - Support the developer - If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) - Gratitudes - These people make Kotatsu become better! - Copyright & Licenses - License + Welcome + Description + Backup saved successfully + Some manufacturers can change the system behavior, which may breaks background tasks. + Read more + Queued + There are currently no active downloads + This chapter is missing on your device. Download or read it online. + Chapter is missing + Translate this app + Translation + Author + Feedback + Topic on 4PDA + Support the developer + If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) + Gratitudes + These people make Kotatsu become better! + Copyright & Licenses + License + Authorization complete + Authorization on %s is not supported + You will be logged out from all sources that you are authorized in \ No newline at end of file diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index fa59bb8f9..6e773ce26 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -14,4 +14,11 @@ android:title="@string/use_ssl" app:iconSpaceReserved="false" /> + + \ No newline at end of file From 4977464e690750fba607a7f2b365ac959e260460 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 8 Sep 2021 07:27:44 +0300 Subject: [PATCH 073/180] ExHentai manga source --- .../kotatsu/base/domain/MangaLoaderContext.kt | 29 +- .../kotatsu/core/model/MangaSource.kt | 1 + .../kotatsu/core/parser/ParserModule.kt | 1 + .../core/parser/site/ExHentaiRepository.kt | 265 ++++++++++++++++++ .../core/parser/site/NineMangaRepository.kt | 2 +- .../kotatsu/utils/ext/CookieJarExt.kt | 37 +++ .../koitharu/kotatsu/utils/ext/ParseExt.kt | 8 +- .../koitharu/kotatsu/utils/ext/StringExt.kt | 8 +- 8 files changed, 320 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt index 821d89e7b..2c8ec865d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaLoaderContext.kt @@ -9,7 +9,7 @@ import org.koitharu.kotatsu.utils.ext.await open class MangaLoaderContext( private val okHttp: OkHttpClient, - private val cookieJar: CookieJar + val cookieJar: CookieJar ) : KoinComponent { suspend fun httpGet(url: String, headers: Headers? = null): Response { @@ -57,33 +57,6 @@ open class MangaLoaderContext( open fun getSettings(source: MangaSource) = SourceSettings(get(), source) - fun insertCookies(domain: String, vararg cookies: String) { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(domain) - .build() - cookieJar.saveFromResponse(url, cookies.mapNotNull { - Cookie.parse(url, it) - }) - } - - fun getCookies(domain: String): List { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(domain) - .build() - return cookieJar.loadForRequest(url) - } - - fun copyCookies(oldDomain: String, newDomain: String) { - val url = HttpUrl.Builder() - .scheme(SCHEME_HTTP) - .host(oldDomain) - val cookies = cookieJar.loadForRequest(url.build()) - url.host(newDomain) - cookieJar.saveFromResponse(url.build(), cookies) - } - private companion object { private const val SCHEME_HTTP = "http" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 3f8f07973..f881a59f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -39,6 +39,7 @@ enum class MangaSource( NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), + EXHENTAI("ExHentai", null, ExHentaiRepository::class.java) ; @get:Throws(NoBeanDefFoundException::class) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index baf3156e3..361bdfa61 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -32,4 +32,5 @@ val parserModule factory(named(MangaSource.NINEMANGA_RU)) { NineMangaRepository.Russian(get()) } factory(named(MangaSource.NINEMANGA_IT)) { NineMangaRepository.Italiano(get()) } factory(named(MangaSource.NINEMANGA_FR)) { NineMangaRepository.Francais(get()) } + factory(named(MangaSource.EXHENTAI)) { ExHentaiRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt new file mode 100644 index 000000000..a699357e8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -0,0 +1,265 @@ +package org.koitharu.kotatsu.core.parser.site + +import org.jsoup.nodes.Element +import org.koitharu.kotatsu.base.domain.MangaLoaderContext +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.utils.ext.* +import kotlin.math.pow + +private const val DOMAIN_UNAUTHORIZED = "e-hentai.org" +private const val DOMAIN_AUTHORIZED = "exhentai.org" + +class ExHentaiRepository( + loaderContext: MangaLoaderContext, +) : RemoteMangaRepository(loaderContext), MangaRepositoryAuthProvider { + + override val source = MangaSource.EXHENTAI + + override val defaultDomain: String + get() = if (isAuthorized()) DOMAIN_AUTHORIZED else DOMAIN_UNAUTHORIZED + + override val authUrl: String + get() = "https://${getDomain()}/bounce_login.php" + + private val ratingPattern = Regex("-?[0-9]+px") + private val authCookies = arrayOf("ipb_member_id", "ipb_pass_hash") + private var updateDm = false + + init { + loaderContext.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "nw=1", "sl=dm_2") + loaderContext.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2") + } + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag?, + ): List = getList(offset, query, setOfNotNull(tag), sortOrder) + + override suspend fun getList( + offset: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder?, + ): List { + val page = (offset / 25f).toIntUp() + var search = query?.urlEncoded().orEmpty() + val url = buildString { + append("https://") + append(getDomain()) + append("/?page=") + append(page) + if (!tags.isNullOrEmpty()) { + var fCats = 0 + for (tag in tags) { + tag.key.toIntOrNull()?.let { fCats = fCats or it } ?: run { + search += tag.key + " " + } + } + if (fCats != 0) { + append("&f_cats=") + append(1023 - fCats) + } + } + if (search.isNotEmpty()) { + append("&f_search=") + append(search.trim().replace(' ', '+')) + } + // by unknown reason cookie "sl=dm_2" is ignored, so, we should request it again + if (updateDm) { + append("&inline_set=dm_e") + } + } + val body = loaderContext.httpGet(url).parseHtml().body() + val root = body.selectFirst("table.itg") + ?.selectFirst("tbody") + ?: if (updateDm) { + parseFailed("Cannot find root") + } else { + updateDm = true + return getList(offset, query, tags, sortOrder) + } + updateDm = false + return root.children().mapNotNull { tr -> + if (tr.childrenSize() != 2) return@mapNotNull null + val (td1, td2) = tr.children() + val glink = td2.selectFirst("div.glink") ?: parseFailed("glink not found") + val a = glink.parents().select("a").first() ?: parseFailed("link not found") + val href = a.relUrl("href") + val tagsDiv = glink.nextElementSibling() ?: parseFailed("tags div not found") + val mainTag = td2.selectFirst("div.cn")?.let { div -> + MangaTag( + title = div.text(), + key = tagIdByClass(div.classNames()) ?: return@let null, + source = source, + ) + } + Manga( + id = generateUid(href), + title = glink.text().cleanupTitle(), + altTitle = null, + url = href, + publicUrl = a.absUrl("href"), + rating = td2.selectFirst("div.ir")?.parseRating() ?: Manga.NO_RATING, + isNsfw = true, + coverUrl = td1.selectFirst("img")?.absUrl("src").orEmpty(), + tags = setOfNotNull(mainTag), + state = null, + author = tagsDiv.getElementsContainingOwnText("artist:").first() + ?.nextElementSibling()?.text(), + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = loaderContext.httpGet(manga.url.withDomain()).parseHtml() + val root = doc.body().selectFirst("div.gm") ?: parseFailed("Cannot find root") + val cover = root.getElementById("gd1")?.children()?.first() + val title = root.getElementById("gd2") + val taglist = root.getElementById("taglist") + val tabs = doc.body().selectFirst("table.ptt")?.selectFirst("tr") + return manga.copy( + title = title?.getElementById("gn")?.text()?.cleanupTitle() ?: manga.title, + altTitle = title?.getElementById("gj")?.text()?.cleanupTitle() ?: manga.altTitle, + publicUrl = doc.baseUri().ifEmpty { manga.publicUrl }, + rating = root.getElementById("rating_label")?.text() + ?.substringAfterLast(' ') + ?.toFloatOrNull() + ?.div(5f) ?: manga.rating, + largeCoverUrl = cover?.css("background")?.cssUrl(), + description = taglist?.select("tr")?.joinToString("
") { tr -> + val (tc, td) = tr.children() + val subtags = td.select("a").joinToString { it.html() } + "${tc.html()} $subtags" + }, + chapters = tabs?.select("a")?.findLast { a -> + a.text().toIntOrNull() != null + }?.let { a -> + val count = a.text().toInt() + val chapters = ArrayList(count) + for (i in 1..count) { + val url = "${manga.url}?p=$i" + chapters += MangaChapter( + id = generateUid(url), + name = "${manga.title} #$i", + number = i, + url = url, + branch = null, + source = source, + ) + } + chapters + }, + ) + } + + override suspend fun getPages(chapter: MangaChapter): List { + val doc = loaderContext.httpGet(chapter.url.withDomain()).parseHtml() + val root = doc.body().getElementById("gdt") ?: parseFailed("Root not found") + return root.select("a").mapNotNull { a -> + val url = a.relUrl("href") + MangaPage( + id = generateUid(url), + url = url, + referer = a.absUrl("href"), + preview = null, + source = source, + ) + } + } + + override suspend fun getPageUrl(page: MangaPage): String { + val doc = loaderContext.httpGet(page.url.withDomain()).parseHtml() + return doc.body().getElementById("img")?.absUrl("src") + ?: parseFailed("Image not found") + } + + override suspend fun getTags(): Set { + val doc = loaderContext.httpGet("https://${getDomain()}").parseHtml() + val root = doc.body().getElementById("searchbox")?.selectFirst("table") + ?: parseFailed("Root not found") + return root.select("div.cs").mapNotNullToSet { div -> + val id = div.id().substringAfterLast('_').toIntOrNull() + ?: return@mapNotNullToSet null + MangaTag( + title = div.text(), + key = id.toString(), + source = source + ) + } + } + + override fun isAuthorized(): Boolean { + val authorized = isAuthorized(DOMAIN_UNAUTHORIZED) + if (authorized) { + if (!isAuthorized(DOMAIN_AUTHORIZED)) { + loaderContext.cookieJar.copyCookies( + DOMAIN_UNAUTHORIZED, + DOMAIN_AUTHORIZED, + authCookies, + ) + loaderContext.cookieJar.insertCookies(DOMAIN_AUTHORIZED, "yay=louder") + } + return true + } + return false + } + + private fun isAuthorized(domain: String): Boolean { + val cookies = loaderContext.cookieJar.getCookies(domain).mapToSet { x -> x.name } + return authCookies.all { it in cookies } + } + + private fun Element.parseRating(): Float { + return runCatching { + val style = requireNotNull(attr("style")) + val (v1, v2) = ratingPattern.find(style)!!.destructured + var p1 = v1.dropLast(2).toInt() + val p2 = v2.dropLast(2).toInt() + if (p2 != -1) { + p1 += 8 + } + (80 - p1) / 80f + }.getOrDefault(Manga.NO_RATING) + } + + private fun String.cleanupTitle(): String { + val result = StringBuilder(length) + var skip = false + for (c in this) { + when { + c == '[' -> skip = true + c == ']' -> skip = false + c.isWhitespace() && result.isEmpty() -> continue + !skip -> result.append(c) + } + } + while (result.lastOrNull()?.isWhitespace() == true) { + result.deleteCharAt(result.lastIndex) + } + return result.toString() + } + + private fun String.cssUrl(): String? { + val fromIndex = indexOf("url(") + if (fromIndex == -1) { + return null + } + val toIndex = indexOf(')', startIndex = fromIndex) + return if (toIndex == -1) { + null + } else { + substring(fromIndex + 4, toIndex).trim() + } + } + + private fun tagIdByClass(classNames: Collection): String? { + val className = classNames.find { x -> x.startsWith("ct") } ?: return null + val num = className.drop(2).toIntOrNull(16) ?: return null + return 2.0.pow(num).toInt().toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index 7fea87b3c..bbb236521 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -16,7 +16,7 @@ abstract class NineMangaRepository( ) : RemoteMangaRepository(loaderContext) { init { - loaderContext.insertCookies(getDomain(), "ninemanga_template_desk=yes") + loaderContext.cookieJar.insertCookies(getDomain(), "ninemanga_template_desk=yes") } override val sortOrders: Set = EnumSet.of( diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt new file mode 100644 index 000000000..d188cadbe --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CookieJarExt.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.utils.ext + +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +private const val SCHEME_HTTPS = "https" + +fun CookieJar.insertCookies(domain: String, vararg cookies: String) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(domain) + .build() + saveFromResponse(url, cookies.mapNotNull { + Cookie.parse(url, it) + }) +} + +fun CookieJar.getCookies(domain: String): List { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(domain) + .build() + return loadForRequest(url) +} + +fun CookieJar.copyCookies(oldDomain: String, newDomain: String, names: Array? = null) { + val url = HttpUrl.Builder() + .scheme(SCHEME_HTTPS) + .host(oldDomain) + var cookies = loadForRequest(url.build()) + if (names != null) { + cookies = cookies.filter { c -> c.name in names } + } + url.host(newDomain) + saveFromResponse(url.build(), cookies) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt index c6e46b05e..7968b5ba2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt @@ -91,4 +91,10 @@ fun Element.relUrl(attributeKey: String): String { return attr.removePrefix(baseUrl.dropLast(1)) } -private val REGEX_URL_BASE = Regex("^[^/]{2,6}://[^/]+/", RegexOption.IGNORE_CASE) \ No newline at end of file +private val REGEX_URL_BASE = Regex("^[^/]{2,6}://[^/]+/", RegexOption.IGNORE_CASE) + +fun Element.css(property: String): String? { + val regex = Regex("${Regex.escape(property)}\\s*:\\s*[^;]+") + val css = attr("style").find(regex) ?: return null + return css.substringAfter(':').removeSuffix(';').trim() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index fbf8326ae..52f774e6f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -6,7 +6,6 @@ import java.math.BigInteger import java.net.URLEncoder import java.security.MessageDigest import java.util.* -import kotlin.contracts.contract import kotlin.math.min fun String.longHashCode(): Long { @@ -158,6 +157,13 @@ fun String.substringBetweenLast(from: String, to: String, fallbackValue: String fun String.find(regex: Regex) = regex.find(this)?.value +fun String.removeSuffix(suffix: Char): String { + if (lastOrNull() == suffix) { + return substring(0, length - 1) + } + return this +} + fun String.levenshteinDistance(other: String): Int { if (this == other) { return 0 From c1b6cef3621972889564f1bab627f07a37b5a63a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 11 Sep 2021 13:50:47 +0300 Subject: [PATCH 074/180] Multiple tags support in (almost) all sources #19 --- .../kotatsu/core/model/MangaFilter.kt | 2 +- .../kotatsu/core/parser/MangaRepository.kt | 16 +----- .../core/parser/site/AnibelRepository.kt | 2 +- .../core/parser/site/ChanRepository.kt | 12 ++-- .../core/parser/site/DesuMeRepository.kt | 10 ++-- .../core/parser/site/ExHentaiRepository.kt | 11 +--- .../core/parser/site/GroupleRepository.kt | 56 ++++++++++++++++--- .../core/parser/site/HenChanRepository.kt | 8 +-- .../core/parser/site/MangaLibRepository.kt | 10 ++-- .../core/parser/site/MangaTownRepository.kt | 15 +++-- .../core/parser/site/MangareadRepository.kt | 11 +++- .../core/parser/site/NineMangaRepository.kt | 4 +- .../core/parser/site/RemangaRepository.kt | 12 ++-- .../kotatsu/list/ui/MangaListFragment.kt | 5 +- .../kotatsu/list/ui/filter/FilterAdapter.kt | 40 ++++++------- .../list/ui/filter/FilterSortHolder.kt | 4 +- .../kotatsu/list/ui/filter/FilterTagHolder.kt | 16 +++--- .../local/domain/LocalMangaRepository.kt | 6 +- .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../remotelist/ui/RemoteListFragment.kt | 1 - .../remotelist/ui/RemoteListViewModel.kt | 4 +- .../search/domain/MangaSearchRepository.kt | 6 +- .../kotatsu/search/ui/SearchViewModel.kt | 5 +- .../koitharu/kotatsu/utils/ext/StringExt.kt | 20 +++++++ .../res/layout/item_checkable_multiple.xml | 13 +++++ .../main/res/layout/item_checkable_single.xml | 1 - .../core/parser/RemoteMangaRepositoryTest.kt | 12 ++-- 27 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 app/src/main/res/layout/item_checkable_multiple.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt index 814c00571..498492f24 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaFilter.kt @@ -6,5 +6,5 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MangaFilter( val sortOrder: SortOrder?, - val tag: MangaTag?, + val tags: Set, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index fc960f069..c8904b2a8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -9,24 +9,12 @@ interface MangaRepository { val sortOrders: Set - suspend fun getList( + suspend fun getList2( offset: Int, query: String? = null, tags: Set? = null, sortOrder: SortOrder? = null, - ): List = if (tags == null || tags.size <= 1) { - getList(offset, query, sortOrder, tags?.singleOrNull()) - } else { - throw NotImplementedError("Multiple filter are not supported by this source yet") - } - - @Deprecated("Use multiple tag variant") - suspend fun getList( - offset: Int, - query: String? = null, - sortOrder: SortOrder? = null, - tag: MangaTag? = null, - ): List = throw NotImplementedError("This is fine") + ): List suspend fun getDetails(manga: Manga): Manga diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 6142f71d3..bb3622afc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -16,7 +16,7 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt index 242686815..629082010 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ChanRepository.kt @@ -17,11 +17,11 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe SortOrder.ALPHABETICAL ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val url = when { @@ -31,7 +31,11 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe } "https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}" } - tag != null -> "https://$domain/tags/${tag.key}&n=${getSortKey2(sortOrder)}?offset=$offset" + !tags.isNullOrEmpty() -> tags.joinToString( + prefix = "https://$domain/tags/", + postfix = "&n=${getSortKey2(sortOrder)}?offset=$offset", + separator = "+", + ) { tag -> tag.key } else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset" } val doc = loaderContext.httpGet(url).parseHtml() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt index 4d62b7af5..73b223b82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/DesuMeRepository.kt @@ -20,11 +20,11 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor SortOrder.ALPHABETICAL ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { if (query != null && offset != 0) { return emptyList() @@ -37,9 +37,9 @@ class DesuMeRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor append(getSortKey(sortOrder)) append("&page=") append((offset / 20) + 1) - if (tag != null) { + if (!tags.isNullOrEmpty()) { append("&genres=") - append(tag.key) + appendAll(tags, ",") { it.key } } if (query != null) { append("&search=") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt index a699357e8..bef2af960 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/ExHentaiRepository.kt @@ -32,14 +32,7 @@ class ExHentaiRepository( loaderContext.cookieJar.insertCookies(DOMAIN_UNAUTHORIZED, "nw=1", "sl=dm_2") } - override suspend fun getList( - offset: Int, - query: String?, - sortOrder: SortOrder?, - tag: MangaTag?, - ): List = getList(offset, query, setOfNotNull(tag), sortOrder) - - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, @@ -80,7 +73,7 @@ class ExHentaiRepository( parseFailed("Cannot find root") } else { updateDm = true - return getList(offset, query, tags, sortOrder) + return getList2(offset, query, tags, sortOrder) } updateDm = false return root.children().mapNotNull { tr -> diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt index 268ad03b0..2d35111e8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/GroupleRepository.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.core.parser.site import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Response import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.model.* @@ -18,11 +19,11 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : SortOrder.RATING ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val doc = when { @@ -33,22 +34,24 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : "offset" to (offset upBy PAGE_SIZE_SEARCH).toString() ) ) - tag == null -> loaderContext.httpGet( + tags.isNullOrEmpty() -> loaderContext.httpGet( "https://$domain/list?sortType=${ getSortKey( sortOrder ) }&offset=${offset upBy PAGE_SIZE}" ) - else -> loaderContext.httpGet( - "https://$domain/list/genre/${tag.key}?sortType=${ + tags.size == 1 -> loaderContext.httpGet( + "https://$domain/list/genre/${tags.first().key}?sortType=${ getSortKey( sortOrder ) }&offset=${offset upBy PAGE_SIZE}" ) - }.parseHtml() - val root = doc.body().getElementById("mangaBox") + offset > 0 -> return emptyList() + else -> advancedSearch(domain, tags) + }.parseHtml().body() + val root = (doc.getElementById("mangaBox") ?: doc.getElementById("mangaResults")) ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") val baseHost = root.baseUri().toHttpUrl().host return root.select("div.tile").mapNotNull { node -> @@ -182,6 +185,43 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) : null -> "updated" } + private suspend fun advancedSearch(domain: String, tags: Set): Response { + val url = "https://$domain/search/advanced" + // Step 1: map catalog genres names to advanced-search genres ids + val tagsIndex = loaderContext.httpGet(url).parseHtml() + .body().selectFirst("form.search-form") + ?.select("div.form-group") + ?.get(1) ?: parseFailed("Genres filter element not found") + val tagNames = tags.map { it.title.lowercase() } + val payload = HashMap() + var foundGenres = 0 + tagsIndex.select("li.property").forEach { li -> + val name = li.text().trim().lowercase() + val id = li.selectFirst("input")?.id() + ?: parseFailed("Id for tag $name not found") + payload[id] = if (name in tagNames) { + foundGenres++ + "in" + } else "" + } + if (foundGenres != tags.size) { + parseFailed("Some genres are not found") + } + // Step 2: advanced search + payload["q"] = "" + payload["s_high_rate"] = "" + payload["s_single"] = "" + payload["s_mature"] = "" + payload["s_completed"] = "" + payload["s_translated"] = "" + payload["s_many_chapters"] = "" + payload["s_wait_upload"] = "" + payload["s_sale"] = "" + payload["years"] = "1900,2099" + payload["+"] = "Искать".urlEncoded() + return loaderContext.httpPost(url, payload) + } + private companion object { private const val PAGE_SIZE = 70 diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt index aaad60566..a358f50df 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/HenChanRepository.kt @@ -11,13 +11,13 @@ class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(load override val defaultDomain = "hentaichan.live" override val source = MangaSource.HENCHAN - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { - return super.getList(offset, query, sortOrder, tag).map { + return super.getList2(offset, query, tags, sortOrder).map { val cover = it.coverUrl if (cover.contains("_blur")) { it.copy(coverUrl = cover.replace("_blur", "")) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt index e2d7fbc5f..ba0ea771d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaLibRepository.kt @@ -26,11 +26,11 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { if (!query.isNullOrEmpty()) { return if (offset == 0) search(query) else emptyList() @@ -43,8 +43,8 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) : append(getSortKey(sortOrder)) append("&page=") append(page) - if (tag != null) { - append("&includeGenres[]=") + tags?.forEach { tag -> + append("&genres[include][]=") append(tag.key) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt index 44fd5937e..bf5ce1b8f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -23,11 +23,11 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : SortOrder.UPDATED ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val sortKey = when (sortOrder) { SortOrder.ALPHABETICAL -> "?name.az" @@ -43,8 +43,13 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : } "/search?name=${query.urlEncoded()}".withDomain() } - tag != null -> "/directory/${tag.key}/$page.htm$sortKey".withDomain() - else -> "/directory/$page.htm$sortKey".withDomain() + tags.isNullOrEmpty() -> "/directory/$page.htm$sortKey".withDomain() + tags.size == 1 -> "/directory/${tags.first().key}/$page.htm$sortKey".withDomain() + else -> tags.joinToString( + prefix = "/search?page=$page".withDomain() + ) { tag -> + "&genres[${tag.key}]=1" + } } val doc = loaderContext.httpGet(url).parseHtml() val root = doc.body().selectFirst("ul.manga_pic_list") diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index ee69048bb..0a94c3d04 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -20,12 +20,17 @@ class MangareadRepository( SortOrder.POPULARITY ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { + val tag = when { + tags.isNullOrEmpty() -> null + tags.size == 1 -> tags.first() + else -> throw NotImplementedError("Multiple genres are not supported by this source") + } val payload = createRequestTemplate() payload["page"] = (offset / PAGE_SIZE.toFloat()).toIntUp().toString() payload["vars[meta_key]"] = when (sortOrder) { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt index bbb236521..9c67b2146 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/NineMangaRepository.kt @@ -23,7 +23,7 @@ abstract class NineMangaRepository( SortOrder.POPULARITY, ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, tags: Set?, @@ -146,7 +146,7 @@ abstract class NineMangaRepository( val cateId = li.attr("cate_id") ?: return@mapNotNullToSet null val a = li.selectFirst("a") ?: return@mapNotNullToSet null MangaTag( - title = a.text(), + title = a.text().toTitleCase(), key = cateId, source = source ) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt index f368db41e..17a3dd5d7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/RemangaRepository.kt @@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.utils.ext.* import java.util.* -import kotlin.collections.ArrayList class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepository(loaderContext) { @@ -24,11 +23,11 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito SortOrder.NEWEST ) - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { val domain = getDomain() val urlBuilder = StringBuilder() @@ -40,8 +39,9 @@ class RemangaRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposito } else { urlBuilder.append("/api/search/catalog/?ordering=") .append(getSortKey(sortOrder)) - if (tag != null) { - urlBuilder.append("&genres=" + tag.key) + tags?.forEach { tag -> + urlBuilder.append("&genres=") + urlBuilder.append(tag.key) } } urlBuilder diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 066da26ec..7c06f6943 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -217,10 +217,7 @@ abstract class MangaListFragment : BaseFragment(), activity?.invalidateOptionsMenu() } - @CallSuper - override fun onFilterChanged(filter: MangaFilter) { - drawer?.closeDrawers() - } + override fun onFilterChanged(filter: MangaFilter) = Unit override fun onWindowInsetsChanged(insets: Insets) { val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt index e7fe78058..983fdf0f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterAdapter.kt @@ -6,19 +6,15 @@ import org.koitharu.kotatsu.base.ui.list.BaseViewHolder import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.SortOrder -import java.util.* class FilterAdapter( - sortOrders: List = emptyList(), - tags: List = emptyList(), + private val sortOrders: List = emptyList(), + private val tags: List = emptyList(), state: MangaFilter?, private val listener: OnFilterChangedListener ) : RecyclerView.Adapter>() { - private val sortOrders = ArrayList(sortOrders) - private val tags = ArrayList(Collections.singletonList(null) + tags) - - private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), null) + private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), emptySet()) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { VIEW_TYPE_SORT -> FilterSortHolder(parent).apply { @@ -28,7 +24,7 @@ class FilterAdapter( } VIEW_TYPE_TAG -> FilterTagHolder(parent).apply { itemView.setOnClickListener { - setCheckedTag(boundData) + setCheckedTag(boundData ?: return@setOnClickListener, !isChecked) } } else -> throw IllegalArgumentException("Unknown viewType $viewType") @@ -44,7 +40,7 @@ class FilterAdapter( } is FilterTagHolder -> { val item = tags[position - sortOrders.size] - holder.bind(item, item == currentState.tag) + holder.bind(item, item in currentState.tags) } } } @@ -54,19 +50,25 @@ class FilterAdapter( else -> VIEW_TYPE_TAG } - fun setCheckedTag(tag: MangaTag?) { - if (tag != currentState.tag) { - val oldItemPos = tags.indexOf(currentState.tag) - val newItemPos = tags.indexOf(tag) - currentState = currentState.copy(tag = tag) - if (oldItemPos in tags.indices) { - notifyItemChanged(sortOrders.size + oldItemPos) + fun setCheckedTag(tag: MangaTag, isChecked: Boolean) { + currentState = if (tag in currentState.tags) { + if (!isChecked) { + currentState.copy(tags = currentState.tags - tag) + } else { + return } - if (newItemPos in tags.indices) { - notifyItemChanged(sortOrders.size + newItemPos) + } else { + if (isChecked) { + currentState.copy(tags = currentState.tags + tag) + } else { + return } - listener.onFilterChanged(currentState) } + val index = tags.indexOf(tag) + if (index in tags.indices) { + notifyItemChanged(sortOrders.size + index) + } + listener.onFilterChanged(currentState) } fun setCheckedSort(sort: SortOrder) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt index a182131b6..9275ae831 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterSortHolder.kt @@ -12,7 +12,7 @@ class FilterSortHolder(parent: ViewGroup) : ) { override fun onBind(data: SortOrder, extra: Boolean) { - binding.radio.setText(data.titleRes) - binding.radio.isChecked = extra + binding.root.setText(data.titleRes) + binding.root.isChecked = extra } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt index f3bf89635..2054d4cb9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterTagHolder.kt @@ -2,18 +2,20 @@ package org.koitharu.kotatsu.list.ui.filter import android.view.LayoutInflater import android.view.ViewGroup -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.BaseViewHolder import org.koitharu.kotatsu.core.model.MangaTag -import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding +import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding class FilterTagHolder(parent: ViewGroup) : - BaseViewHolder( - ItemCheckableSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false) + BaseViewHolder( + ItemCheckableMultipleBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) { - override fun onBind(data: MangaTag?, extra: Boolean) { - binding.radio.text = data?.title ?: context.getString(R.string.all) - binding.radio.isChecked = extra + val isChecked: Boolean + get() = binding.root.isChecked + + override fun onBind(data: MangaTag, extra: Boolean) { + binding.root.text = data.title + binding.root.isChecked = extra } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 383c295bc..3d4c51571 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -25,11 +25,11 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { private val filenameFilter = CbzFilter() - override suspend fun getList( + override suspend fun getList2( offset: Int, query: String?, - sortOrder: SortOrder?, - tag: MangaTag? + tags: Set?, + sortOrder: SortOrder? ): List { require(offset == 0) { "LocalMangaRepository does not support pagination" diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 844cbcbbc..5c12ad115 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -61,7 +61,7 @@ class LocalListViewModel( launchLoadingJob(Dispatchers.Default) { try { listError.value = null - mangaList.value = repository.getList(0, tags = null) + mangaList.value = repository.getList2(0) } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 2ecb7f7bf..1f9b543a6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -31,7 +31,6 @@ class RemoteListFragment : MangaListFragment() { override fun onFilterChanged(filter: MangaFilter) { viewModel.applyFilter(filter) - super.onFilterChanged(filter) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index aaf6ccaa3..870480e35 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -78,10 +78,10 @@ class RemoteListViewModel( loadingJob = launchLoadingJob(Dispatchers.Default) { try { listError.value = null - val list = repository.getList( + val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, sortOrder = appliedFilter?.sortOrder, - tag = appliedFilter?.tag + tags = appliedFilter?.tags, ) if (!append) { mangaList.value = list diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index a58e5098c..d099f54c5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -29,7 +29,11 @@ class MangaSearchRepository( MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() .flatMapMerge(concurrency) { source -> runCatching { - source.repository.getList(0, query, SortOrder.POPULARITY) + source.repository.getList2( + offset = 0, + query = query, + sortOrder = SortOrder.POPULARITY + ) }.getOrElse { emptyList() }.asFlow() diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 35df96c82..659e12082 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -71,10 +71,9 @@ class SearchViewModel( loadingJob = launchLoadingJob(Dispatchers.Default) { try { listError.value = null - val list = repository.getList( + val list = repository.getList2( offset = if (append) mangaList.value?.size ?: 0 else 0, - tags = null, - query = query + query = query, ) if (!append) { mangaList.value = list diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index 52f774e6f..05d310506 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -48,6 +48,10 @@ fun String.toCamelCase(): String { return result.toString() } +fun String.toTitleCase(): String { + return replaceFirstChar { x -> x.uppercase() } +} + fun String.transliterate(skipMissing: Boolean): String { val cyr = charArrayOf( 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п', @@ -200,4 +204,20 @@ fun String.levenshteinDistance(other: String): Int { } return cost[lhsLength - 1] +} + +inline fun StringBuilder.appendAll( + items: Iterable, + separator: CharSequence, + transform: (T) -> CharSequence = { it.toString() }, +) { + var isFirst = true + for (item in items) { + if (isFirst) { + isFirst = false + } else { + append(separator) + } + append(transform(item)) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_multiple.xml b/app/src/main/res/layout/item_checkable_multiple.xml new file mode 100644 index 000000000..14b97b714 --- /dev/null +++ b/app/src/main/res/layout/item_checkable_multiple.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkable_single.xml b/app/src/main/res/layout/item_checkable_single.xml index af15894cd..12eb91d8d 100644 --- a/app/src/main/res/layout/item_checkable_single.xml +++ b/app/src/main/res/layout/item_checkable_single.xml @@ -2,7 +2,6 @@ Date: Sat, 11 Sep 2021 16:01:15 +0300 Subject: [PATCH 075/180] Show current filter in list header --- .../kotatsu/base/ui/widgets/ChipsView.kt | 17 +++++++++++++- .../kotatsu/list/ui/MangaListFragment.kt | 10 ++++++-- .../kotatsu/list/ui/MangaListViewModel.kt | 3 +++ .../list/ui/adapter/CurrentFilterAD.kt | 23 +++++++++++++++++++ .../list/ui/adapter/MangaListAdapter.kt | 6 ++++- .../list/ui/model/CurrentFilterModel.kt | 7 ++++++ .../remotelist/ui/RemoteListViewModel.kt | 19 ++++++++++++++- .../main/res/layout/item_current_filter.xml | 15 ++++++++++++ app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt create mode 100644 app/src/main/res/layout/item_current_filter.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt index 84a34261e..eb867ec0f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/ChipsView.kt @@ -22,12 +22,21 @@ class ChipsView @JvmOverloads constructor( private var chipOnClickListener = OnClickListener { onChipClickListener?.onChipClick(it as Chip, it.tag) } + private var chipOnCloseListener = OnClickListener { + onChipCloseClickListener?.onChipCloseClick(it as Chip, it.tag) + } var onChipClickListener: OnChipClickListener? = null set(value) { field = value val isChipClickable = value != null children.forEach { it.isClickable = isChipClickable } } + var onChipCloseClickListener: OnChipCloseClickListener? = null + set(value) { + field = value + val isCloseIconVisible = value != null + children.forEach { (it as? Chip)?.isCloseIconVisible = isCloseIconVisible } + } override fun requestLayout() { if (isLayoutSuppressedCompat) { @@ -69,7 +78,8 @@ class ChipsView @JvmOverloads constructor( val drawable = ChipDrawable.createFromAttributes(context, null, 0, R.style.Widget_Kotatsu_Chip) chip.setChipDrawable(drawable) chip.setTextColor(ContextCompat.getColor(context, R.color.color_primary)) - chip.isCloseIconVisible = false + chip.isCloseIconVisible = onChipCloseClickListener != null + chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setEnsureMinTouchTargetSize(false) chip.setOnClickListener(chipOnClickListener) addView(chip) @@ -96,4 +106,9 @@ class ChipsView @JvmOverloads constructor( fun onChipClick(chip: Chip, data: Any?) } + + fun interface OnChipCloseClickListener { + + fun onChipCloseClick(chip: Chip, data: Any?) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 7c06f6943..1fa44344c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -71,7 +71,13 @@ abstract class MangaListFragment : BaseFragment(), super.onViewCreated(view, savedInstanceState) drawer = binding.root as? DrawerLayout drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this, ::resolveException) + listAdapter = MangaListAdapter( + coil = get(), + lifecycleOwner = viewLifecycleOwner, + clickListener = this, + onRetryClick = ::resolveException, + onTagRemoveClick = viewModel::onRemoveFilterTag + ) paginationListener = PaginationScrollListener(4, this) with(binding.recyclerView) { setHasFixedSize(true) @@ -287,7 +293,7 @@ abstract class MangaListFragment : BaseFragment(), final override fun getSectionTitle(position: Int): CharSequence? { return when (binding.recyclerViewFilter.adapter?.getItemViewType(position)) { FilterAdapter.VIEW_TYPE_SORT -> getString(R.string.sort_order) - FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genre) + FilterAdapter.VIEW_TYPE_TAG -> getString(R.string.genres) else -> null } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 0fa365629..3d94d1b65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.list.ui.model.ListModel @@ -36,6 +37,8 @@ abstract class MangaListViewModel( } } + open fun onRemoveFilterTag(tag: MangaTag) = Unit + abstract fun onRefresh() abstract fun onRetry() diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt new file mode 100644 index 000000000..4848a27b8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt @@ -0,0 +1,23 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.base.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.databinding.ItemCurrentFilterBinding +import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel +import org.koitharu.kotatsu.list.ui.model.ListModel + +fun currentFilterAD( + onTagRemoveClick: (MangaTag) -> Unit, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCurrentFilterBinding.inflate(inflater, parent, false) } +) { + + binding.chipsTags.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> + onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener) + } + + bind { + binding.chipsTags.setChips(item.chips) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 71b4c2064..cc4c80967 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -6,6 +6,7 @@ import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel @@ -17,7 +18,8 @@ class MangaListAdapter( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, - onRetryClick: (Throwable) -> Unit + onRetryClick: (Throwable) -> Unit, + onTagRemoveClick: (MangaTag) -> Unit, ) : AsyncListDifferDelegationAdapter(DiffCallback()) { init { @@ -38,6 +40,7 @@ class MangaListAdapter( .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(onRetryClick)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) + .addDelegate(ITEM_TYPE_FILTER, currentFilterAD(onTagRemoveClick)) } fun setItems(list: List, commitCallback: Runnable) { @@ -79,5 +82,6 @@ class MangaListAdapter( const val ITEM_TYPE_ERROR_FOOTER = 7 const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_HEADER = 9 + const val ITEM_TYPE_FILTER = 10 } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt new file mode 100644 index 000000000..32cebb25c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/CurrentFilterModel.kt @@ -0,0 +1,7 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.base.ui.widgets.ChipsView + +data class CurrentFilterModel( + val chips: Collection, +) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 870480e35..d8df693a3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -7,8 +7,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaFilter +import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings @@ -40,8 +42,9 @@ class RemoteListViewModel( list == null -> listOf(LoadingState) list.isEmpty() -> listOf(EmptyState(R.drawable.ic_book_cross, R.string.nothing_found, R.string._empty)) else -> { - val result = ArrayList(list.size + 2) + val result = ArrayList(list.size + 3) result += headerModel + createFilterModel()?.let { result.add(it) } list.toUi(result, mode) when { error != null -> result += error.toErrorFooter() @@ -65,6 +68,16 @@ class RemoteListViewModel( loadList(append = !mangaList.value.isNullOrEmpty()) } + override fun onRemoveFilterTag(tag: MangaTag) { + val filter = appliedFilter ?: return + if (tag !in filter.tags) { + return + } + applyFilter( + filter.copy(tags = filter.tags - tag) + ) + } + fun loadNextPage() { if (hasNextPage.value && listError.value == null) { loadList(append = true) @@ -108,6 +121,10 @@ class RemoteListViewModel( } } + private fun createFilterModel() = appliedFilter?.run { + CurrentFilterModel(tags.map { ChipsView.ChipModel(0, it.title, it) }) + } + private fun loadFilter() { launchJob(Dispatchers.Default) { try { diff --git a/app/src/main/res/layout/item_current_filter.xml b/app/src/main/res/layout/item_current_filter.xml new file mode 100644 index 000000000..1014b0fdd --- /dev/null +++ b/app/src/main/res/layout/item_current_filter.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 35be37286..862eb79f5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -236,4 +236,5 @@ Авторизация выполнена Авторизация в %s не поддерживается Вы выйдете из всех источников, в которых Вы авторизованы + Жанры \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f82f4b5d9..e09f1ba22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,4 +239,5 @@ Authorization complete Authorization on %s is not supported You will be logged out from all sources that you are authorized in + Genres \ No newline at end of file From f9cee7a8f58231b8a1970fc98c31b3bf0e3054c7 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 13 Sep 2021 08:36:05 +0300 Subject: [PATCH 076/180] Update gradle and dependencies --- app/build.gradle | 12 ++++++------ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d69a4cef4..cb82e64e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 30 - versionCode 367 - versionName '1.1.2' + versionCode 368 + versionName '2.0-a1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -66,8 +66,8 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.activity:activity-ktx:1.3.1' @@ -82,7 +82,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.preference:preference-ktx:1.1.1' - implementation 'androidx.work:work-runtime-ktx:2.6.0-rc01' + implementation 'androidx.work:work-runtime-ktx:2.6.0' implementation 'com.google.android.material:material:1.4.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.3.1' @@ -108,7 +108,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.3' testImplementation 'org.json:json:20210307' - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' testImplementation 'io.insert-koin:koin-test-junit4:3.1.2' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/build.gradle b/build.gradle index cd35015e3..9fb759f76 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.0.2' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 821394777..24415a607 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ -#Sat Jul 03 12:50:59 EEST 2021 +#Mon Sep 13 08:29:18 EEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionSha256Sum=765442b8069c6bee2ea70713861c027587591c6b1df2c857a23361512560894e +# https://gradle.org/release-checksums/ +distributionSha256Sum=0e46229820205440b48a5501122002842b82886e76af35f0f3a069243dca4b3c From 71f205ca8b409637e24115767b8b7fd47e88cd0d Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Sun, 12 Sep 2021 15:37:41 +0300 Subject: [PATCH 077/180] Fix AMOLED theme --- .../koitharu/kotatsu/base/ui/BaseActivity.kt | 4 ++ .../koitharu/kotatsu/main/ui/MainActivity.kt | 47 +++++++++++++++---- app/src/main/res/drawable/tabs_background.xml | 2 +- app/src/main/res/values-night/colors.xml | 8 ++-- app/src/main/res/values-night/themes.xml | 4 +- .../main/res/values-notnight-v23/colors.xml | 5 ++ app/src/main/res/values/colors.xml | 15 ++++-- app/src/main/res/values/themes.xml | 6 +-- 8 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 app/src/main/res/values-notnight-v23/colors.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt index 8fbaa7ad4..ff3d20ea9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseActivity.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.base.ui +import android.graphics.Color import android.os.Build import android.os.Bundle import android.view.KeyEvent @@ -57,6 +58,9 @@ abstract class BaseActivity : AppCompatActivity(), OnApplyWindo this.binding = binding super.setContentView(binding.root) val toolbar = (binding.root.findViewById(R.id.toolbar) as? Toolbar) + if (get().isAmoledTheme) { + toolbar?.setBackgroundColor(Color.BLACK) + } toolbar?.let(this::setSupportActionBar) ViewCompat.setOnApplyWindowInsetsListener(binding.root, this) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 4272a2b5e..096481624 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -8,7 +8,7 @@ import android.os.Build import android.os.Bundle import android.view.MenuItem import android.view.View -import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat @@ -30,6 +30,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSection +import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.NavigationHeaderBinding import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -86,6 +87,13 @@ class MainActivity : BaseActivity(), binding.drawer.addDrawerListener(drawerToggle) supportActionBar?.setDisplayHomeAsUpEnabled(true) + if (get().isAmoledTheme) { + binding.appbar.setBackgroundColor(Color.BLACK) + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background)) + } else { + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface)) + } + with(binding.searchView) { onFocusChangeListener = this@MainActivity searchSuggestionListener = this@MainActivity @@ -205,17 +213,15 @@ class MainActivity : BaseActivity(), } override fun onWindowInsetsChanged(insets: Insets) { - binding.toolbarCard.updateLayoutParams { + binding.toolbarCard.updateLayoutParams { topMargin = insets.top + resources.resolveDp(8) - leftMargin = insets.left + resources.resolveDp(16) - rightMargin = insets.right + resources.resolveDp(16) } - binding.fab.updateLayoutParams { + binding.fab.updateLayoutParams { bottomMargin = insets.bottom + topMargin leftMargin = insets.left + topMargin rightMargin = insets.right + topMargin } - binding.container.updateLayoutParams { + binding.container.updateLayoutParams { topMargin = -(binding.appbar.measureHeight()) } } @@ -337,19 +343,40 @@ class MainActivity : BaseActivity(), private fun onSearchOpened() { binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawerToggle.isDrawerIndicatorEnabled = false - // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/dark - binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_on_secondary)) - binding.toolbarCard.cardElevation = 0f + // Avoiding shadows on the sides if the color is transparent, so we make the AppBarLayout white/grey/black + if (get().isAmoledTheme) { + binding.toolbar.setBackgroundColor(Color.BLACK) + } else { + binding.appbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_surface)) + } + binding.toolbarCard.apply { + cardElevation = 0f + // Remove margin + updateLayoutParams { + leftMargin = 0 + rightMargin = 0 + } + + } binding.appbar.elevation = searchViewElevation } private fun onSearchClosed() { binding.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) drawerToggle.isDrawerIndicatorEnabled = true + if (get().isAmoledTheme) { + binding.toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.color_background)) + } // Returning transparent color binding.appbar.setBackgroundColor(Color.TRANSPARENT) binding.appbar.elevation = 0f - binding.toolbarCard.cardElevation = searchViewElevation + binding.toolbarCard.apply { + cardElevation = searchViewElevation + updateLayoutParams { + leftMargin = resources.resolveDp(16) + rightMargin = resources.resolveDp(16) + } + } } private fun onFirstStart() { diff --git a/app/src/main/res/drawable/tabs_background.xml b/app/src/main/res/drawable/tabs_background.xml index d9df3de10..9743b040f 100644 --- a/app/src/main/res/drawable/tabs_background.xml +++ b/app/src/main/res/drawable/tabs_background.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android"> - + @android:color/black #2EFFFFFF - #2a2b2e + #272727 + #121212 #2EFFFFFF - #B3000000 - - + #B3121212 + @color/system_ui_scrim_dark #1fffffff diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index e4fc0c120..1c0c88d29 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,7 +2,9 @@