Compare commits
1265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42bb5a65ab | ||
|
|
0c37265a5b | ||
|
|
7a65ae3ea7 | ||
|
|
ee027cd64f | ||
|
|
7b2bb5ea8f | ||
|
|
eff2d6bcb6 | ||
|
|
08acf2d882 | ||
|
|
1d78c64350 | ||
|
|
321a9ecf62 | ||
|
|
439a01c43f | ||
|
|
3a9d0def7d | ||
|
|
e4c80b4443 | ||
|
|
940d448e00 | ||
|
|
5ab48a7545 | ||
|
|
cb2bdbdd9a | ||
|
|
8fdaf92cc4 | ||
|
|
0416077964 | ||
|
|
7b60ed6bad | ||
|
|
619be69580 | ||
|
|
9f3c3f8985 | ||
|
|
f345977858 | ||
|
|
9610caf002 | ||
|
|
b75220a1b7 | ||
|
|
ab2a6f5a17 | ||
|
|
2aeefc607b | ||
|
|
9af769bc69 | ||
|
|
46b78cfcd7 | ||
|
|
c24324de9a | ||
|
|
48b9c1236d | ||
|
|
c69d293caa | ||
|
|
0f4cca0e07 | ||
|
|
d6500b8fec | ||
|
|
86140cab1e | ||
|
|
90dfc84119 | ||
|
|
6a792f8ac3 | ||
|
|
c81e8749b6 | ||
|
|
5fa260a0c7 | ||
|
|
e0ba4e2686 | ||
|
|
f188d1c0f3 | ||
|
|
6de55afa27 | ||
|
|
21dcb5b754 | ||
|
|
9b3ea57db1 | ||
|
|
032a8607ba | ||
|
|
f7303c5957 | ||
|
|
d696606ef9 | ||
|
|
0a6e106a1d | ||
|
|
de1a7f0ca8 | ||
|
|
9d31e76cc7 | ||
|
|
20910ffb5d | ||
|
|
7497ee6364 | ||
|
|
0f2ed50e18 | ||
|
|
ba066b577b | ||
|
|
4496fe876f | ||
|
|
a9f5abebf0 | ||
|
|
bebee2ef27 | ||
|
|
4ec2b0c8fe | ||
|
|
4a7be70898 | ||
|
|
2bcba1eb21 | ||
|
|
feca7ba3fc | ||
|
|
745b349e5e | ||
|
|
13946783a5 | ||
|
|
84e5400522 | ||
|
|
02c9a933d2 | ||
|
|
92af851d3b | ||
|
|
009eb9fe44 | ||
|
|
fc8a5ccd9f | ||
|
|
91f46de547 | ||
|
|
d548993e14 | ||
|
|
4f32664b33 | ||
|
|
71b14a3aa8 | ||
|
|
183a61272e | ||
|
|
f1f208ad15 | ||
|
|
c6983d794c | ||
|
|
8228153c83 | ||
|
|
844bd13a07 | ||
|
|
60a5620134 | ||
|
|
dd09a39077 | ||
|
|
1511bd3279 | ||
|
|
259c335607 | ||
|
|
86367b6d3b | ||
|
|
19b893738d | ||
|
|
d817ae0394 | ||
|
|
d81c22b586 | ||
|
|
cd23b044df | ||
|
|
4922881343 | ||
|
|
ff0d04bea6 | ||
|
|
97de629c3b | ||
|
|
7b482e5bcf | ||
|
|
fd575b8131 | ||
|
|
c77e023bef | ||
|
|
a3cf52859b | ||
|
|
5e55bce529 | ||
|
|
b1ba70bf77 | ||
|
|
b930272221 | ||
|
|
75305c0b94 | ||
|
|
24b16e2ce2 | ||
|
|
0ccbba6787 | ||
|
|
ca314867f2 | ||
|
|
236e284360 | ||
|
|
e9a09b6be4 | ||
|
|
9e1be337ed | ||
|
|
104f2ebfae | ||
|
|
6a2e12dc29 | ||
|
|
9587cb439c | ||
|
|
c42d0824b0 | ||
|
|
09f6dd9b4e | ||
|
|
b494c96e31 | ||
|
|
0f6d56ee2d | ||
|
|
8d15691e17 | ||
|
|
bd8b251934 | ||
|
|
2f1b74e45a | ||
|
|
73217b8e11 | ||
|
|
759df969c9 | ||
|
|
466e35fffa | ||
|
|
f44db3dbff | ||
|
|
315870abcb | ||
|
|
3e46b3957c | ||
|
|
6dc81468d2 | ||
|
|
56bc0dbf07 | ||
|
|
7bc33adca8 | ||
|
|
c8794d59f7 | ||
|
|
9c2a57812e | ||
|
|
6bd5033858 | ||
|
|
e7c2a76219 | ||
|
|
0934363298 | ||
|
|
de29527805 | ||
|
|
f11e964f0b | ||
|
|
61a98f54b9 | ||
|
|
50e67daea4 | ||
|
|
0030706226 | ||
|
|
056ef5433d | ||
|
|
c14b2ceeff | ||
|
|
ff2cf9d18a | ||
|
|
96b6900c70 | ||
|
|
c6228d3fe1 | ||
|
|
8ac95e1608 | ||
|
|
69a9ec354b | ||
|
|
0639d3e6c1 | ||
|
|
ae5cebd42d | ||
|
|
cd8381cbfb | ||
|
|
3132049a63 | ||
|
|
bc3a7fc211 | ||
|
|
e794f84c6f | ||
|
|
76709dda21 | ||
|
|
6dc460bc20 | ||
|
|
c2ee548f0a | ||
|
|
1847759ec3 | ||
|
|
02d5dfb375 | ||
|
|
12d8d3e2d1 | ||
|
|
b5705b45df | ||
|
|
46b797fc67 | ||
|
|
5ec7fbed94 | ||
|
|
b48c6d7d38 | ||
|
|
da4aedca97 | ||
|
|
32695f9816 | ||
|
|
bece4cc15d | ||
|
|
548c41fbf9 | ||
|
|
ef9b16da0b | ||
|
|
5d1ef983e9 | ||
|
|
eb78a776cf | ||
|
|
661e502003 | ||
|
|
8c5c7d6b04 | ||
|
|
b1187c611a | ||
|
|
893ba37c86 | ||
|
|
b1bc94b1e9 | ||
|
|
2e3be00e26 | ||
|
|
84f41810c5 | ||
|
|
f0a4fa4e95 | ||
|
|
0c132a521e | ||
|
|
3d05541f61 | ||
|
|
2442e7cbe1 | ||
|
|
4522c478cb | ||
|
|
6881c22453 | ||
|
|
5a0c54e00f | ||
|
|
47f346b42c | ||
|
|
dc358ae6a2 | ||
|
|
bfa9feaef0 | ||
|
|
b5cd92fb5f | ||
|
|
08e5c148fd | ||
|
|
8323d399ff | ||
|
|
5108f45111 | ||
|
|
bf0d34e9cf | ||
|
|
c3216871ed | ||
|
|
a8f5714b35 | ||
|
|
84567767a0 | ||
|
|
eb7efaaac9 | ||
|
|
3729b5f2f0 | ||
|
|
e4c2797f06 | ||
|
|
e02899c3f2 | ||
|
|
96c89a716e | ||
|
|
65ed5c7e6b | ||
|
|
3778a9e1d4 | ||
|
|
71ecd9d8e2 | ||
|
|
7cba8d2dc7 | ||
|
|
79c2927da2 | ||
|
|
a4a28c7342 | ||
|
|
3f96f34b8e | ||
|
|
43a92bdf08 | ||
|
|
51ff1ff7b7 | ||
|
|
2e0eb5de54 | ||
|
|
4f68e7d0e6 | ||
|
|
c6d303980b | ||
|
|
18e6ee1453 | ||
|
|
09144d7f55 | ||
|
|
05583504ee | ||
|
|
8dc02967fc | ||
|
|
4c206746c9 | ||
|
|
e9b0e9f740 | ||
|
|
6d0cd49db3 | ||
|
|
e69964d1f5 | ||
|
|
f368277d73 | ||
|
|
f3a8eb3216 | ||
|
|
f7a585ef55 | ||
|
|
96d6f8d8e6 | ||
|
|
67bbd3e6d3 | ||
|
|
e7afe1c802 | ||
|
|
f63beba8af | ||
|
|
4ddeb1acce | ||
|
|
10b178aed3 | ||
|
|
514594edb0 | ||
|
|
b2a9f7d594 | ||
|
|
f38220eefb | ||
|
|
30ad67ef52 | ||
|
|
810434fef5 | ||
|
|
26a7a7a2e8 | ||
|
|
4d8da40885 | ||
|
|
248bf8ed03 | ||
|
|
090f7a5858 | ||
|
|
5f38b01fd1 | ||
|
|
2169ee7a5b | ||
|
|
d633204efa | ||
|
|
2b12dbd8d7 | ||
|
|
8bfdc73072 | ||
|
|
be666b7854 | ||
|
|
52655cad2c | ||
|
|
8e856211aa | ||
|
|
e1f82d147c | ||
|
|
023605e246 | ||
|
|
c0544e25af | ||
|
|
235b02870b | ||
|
|
25e7ab2d8e | ||
|
|
2d171657dc | ||
|
|
ac9680b5c0 | ||
|
|
42df607f52 | ||
|
|
fc1755612b | ||
|
|
1ead369ee2 | ||
|
|
07aa04aa4d | ||
|
|
7b8bbf9fe1 | ||
|
|
8883e73971 | ||
|
|
0cb1238143 | ||
|
|
f4628f7ab5 | ||
|
|
3d0b69024d | ||
|
|
12e9fb5aab | ||
|
|
5fc08d9ecb | ||
|
|
e7eb61e3e5 | ||
|
|
dae3982e67 | ||
|
|
45b2d2bebe | ||
|
|
77fa34835f | ||
|
|
cee68069d6 | ||
|
|
48afc8624b | ||
|
|
b9b41ed491 | ||
|
|
632b42ea86 | ||
|
|
427272aac1 | ||
|
|
41ac50c76a | ||
|
|
f05bb20428 | ||
|
|
78f417ebe1 | ||
|
|
3fd6bec433 | ||
|
|
262e26a0cc | ||
|
|
1b64c2a330 | ||
|
|
5ea0ecbd12 | ||
|
|
f9a1d1617e | ||
|
|
10d8365fc1 | ||
|
|
85065c57a1 | ||
|
|
75e130b97c | ||
|
|
df99eec429 | ||
|
|
3a40820991 | ||
|
|
27bd2e74ca | ||
|
|
a308688a2e | ||
|
|
259c845912 | ||
|
|
a89ff4d15d | ||
|
|
3ed9ed8cab | ||
|
|
40b9577e69 | ||
|
|
b87ae19712 | ||
|
|
1dc7e61dbd | ||
|
|
7bd1affe5e | ||
|
|
068196b48b | ||
|
|
89ed09fd09 | ||
|
|
f9b233d8c0 | ||
|
|
9017dae834 | ||
|
|
aabae06515 | ||
|
|
ad530fe55d | ||
|
|
703a5358c2 | ||
|
|
4824a95375 | ||
|
|
d266ffddd3 | ||
|
|
fd7e7eb974 | ||
|
|
ca3e789340 | ||
|
|
94ca2aae89 | ||
|
|
0aad55eea0 | ||
|
|
ee95679c60 | ||
|
|
21bcb293f5 | ||
|
|
648ee3c763 | ||
|
|
ac8283d4af | ||
|
|
ae460720e3 | ||
|
|
e66eedf0a1 | ||
|
|
e6891cc3ba | ||
|
|
5b047b616a | ||
|
|
d3c0d89fe0 | ||
|
|
ccebe2660f | ||
|
|
2efe739a43 | ||
|
|
3919884723 | ||
|
|
5407587de2 | ||
|
|
29ead727bb | ||
|
|
cfbe394bfb | ||
|
|
9914b9a312 | ||
|
|
6548a6f1fe | ||
|
|
5a22e9b0e6 | ||
|
|
85ce118141 | ||
|
|
d4c3a815a1 | ||
|
|
e1b9f41fe3 | ||
|
|
55f2f80486 | ||
|
|
628944a4f2 | ||
|
|
7d0c50d58e | ||
|
|
c989061576 | ||
|
|
5896d7abe7 | ||
|
|
1999f6f1a1 | ||
|
|
745f0adf5b | ||
|
|
32b1ee9e7b | ||
|
|
85710acb3a | ||
|
|
ffd31dbea9 | ||
|
|
c4d8cd81b2 | ||
|
|
c4ba311087 | ||
|
|
277d575485 | ||
|
|
f32ff00b68 | ||
|
|
bd5b6beb72 | ||
|
|
c8053b2eb6 | ||
|
|
938be67cd3 | ||
|
|
f608dd3078 | ||
|
|
72169e71ce | ||
|
|
8ce5e7eccf | ||
|
|
cfd39e615a | ||
|
|
8718b8781d | ||
|
|
d18c90c31a | ||
|
|
16c3e61984 | ||
|
|
dba506cb42 | ||
|
|
0186517175 | ||
|
|
f63ccf2e90 | ||
|
|
dfb88feaf0 | ||
|
|
c53ee01af5 | ||
|
|
149b171eda | ||
|
|
c70a2487d9 | ||
|
|
2e56fcb5da | ||
|
|
04533aa347 | ||
|
|
d258f479b3 | ||
|
|
b81ecda43e | ||
|
|
f42e3d7912 | ||
|
|
e1780b71ae | ||
|
|
57bad3814d | ||
|
|
07de0c9c84 | ||
|
|
d8a7280b3b | ||
|
|
7bea3caa07 | ||
|
|
bd27eb9f59 | ||
|
|
056538a341 | ||
|
|
865f335b25 | ||
|
|
6b1e89eda8 | ||
|
|
0dbaf919e2 | ||
|
|
c2508bbae8 | ||
|
|
4c347862f8 | ||
|
|
21f6a0a8b9 | ||
|
|
7431f46117 | ||
|
|
48ac417189 | ||
|
|
98453c34a7 | ||
|
|
7bec47b4d8 | ||
|
|
c62e29d995 | ||
|
|
4d0bd9538b | ||
|
|
fdb4e5098e | ||
|
|
758d3c55d4 | ||
|
|
f40ff12250 | ||
|
|
43d55cedae | ||
|
|
bc4dd1c507 | ||
|
|
b45147563a | ||
|
|
527e11e65b | ||
|
|
9b8b6d789e | ||
|
|
0e4575356a | ||
|
|
4744a0a162 | ||
|
|
b1a94c0f34 | ||
|
|
f38ff55aea | ||
|
|
efc4bbacb5 | ||
|
|
b4e0704a3a | ||
|
|
8294eb4ecd | ||
|
|
28e3f1c063 | ||
|
|
072cdc35e8 | ||
|
|
1ad64f2710 | ||
|
|
b2266d47df | ||
|
|
c8141c6046 | ||
|
|
cc698cc82d | ||
|
|
472a8d9d72 | ||
|
|
4f3721beea | ||
|
|
346526267e | ||
|
|
6c82c6e9f5 | ||
|
|
11dabd7426 | ||
|
|
056a26b55c | ||
|
|
f1863ddc71 | ||
|
|
437ae4cdae | ||
|
|
98f5615d77 | ||
|
|
44ce3ce66d | ||
|
|
080c2724cd | ||
|
|
43872ffe01 | ||
|
|
5cfad9ab8a | ||
|
|
866f9272ef | ||
|
|
c6446afab1 | ||
|
|
f5a6e1e124 | ||
|
|
5595bc6971 | ||
|
|
e6ed353211 | ||
|
|
4e10908015 | ||
|
|
087ececfdd | ||
|
|
c090018acd | ||
|
|
5f6256a5c6 | ||
|
|
9e6be12707 | ||
|
|
737ca4a916 | ||
|
|
b2958d03e4 | ||
|
|
af8550744f | ||
|
|
2f5fd71bb1 | ||
|
|
271750ad93 | ||
|
|
0281c09dde | ||
|
|
c50fa8f10c | ||
|
|
f2ac3c331c | ||
|
|
4fc56f9786 | ||
|
|
da47dac3f7 | ||
|
|
d2afd36656 | ||
|
|
1316d71d3e | ||
|
|
a13c498d00 | ||
|
|
e15934bdc6 | ||
|
|
4ec50f83d2 | ||
|
|
d0b9412559 | ||
|
|
a3b22e050f | ||
|
|
9adf209445 | ||
|
|
5d2395b569 | ||
|
|
672a1e9b2a | ||
|
|
29114ae8a7 | ||
|
|
47f80085d1 | ||
|
|
73c1d2a616 | ||
|
|
35366ac660 | ||
|
|
dc2dd4e3c9 | ||
|
|
66817ae545 | ||
|
|
b6e3cb929b | ||
|
|
6f29259395 | ||
|
|
c520699f9f | ||
|
|
c09b0150ac | ||
|
|
d7c31f3b3b | ||
|
|
362629bb9a | ||
|
|
4ec4421f69 | ||
|
|
029815e0d7 | ||
|
|
019b41a9f9 | ||
|
|
a56e977058 | ||
|
|
f436a49e5f | ||
|
|
652351f79a | ||
|
|
b6bfef6b50 | ||
|
|
c119db67e9 | ||
|
|
08e036f9fb | ||
|
|
07519b82f3 | ||
|
|
2644756a01 | ||
|
|
f6c715c5a7 | ||
|
|
81f3a40ba8 | ||
|
|
736be6249c | ||
|
|
0add49f32c | ||
|
|
1e2be37fd6 | ||
|
|
529c6c7a08 | ||
|
|
03251cbf9a | ||
|
|
4ab9ace2f2 | ||
|
|
c55be4efc5 | ||
|
|
48b01d0706 | ||
|
|
e2e0d7a53d | ||
|
|
e3a67940d0 | ||
|
|
5ce2bc92d6 | ||
|
|
d05e777b2c | ||
|
|
206673a417 | ||
|
|
95e46249c5 | ||
|
|
ea9ae2263c | ||
|
|
2acbff487e | ||
|
|
26b852365a | ||
|
|
c2e56f7ba6 | ||
|
|
68e8876288 | ||
|
|
5c44a4dbb3 | ||
|
|
7a7ba802f6 | ||
|
|
c5ae9fb087 | ||
|
|
e0f23d2e6d | ||
|
|
e9a972eec9 | ||
|
|
155af8889b | ||
|
|
61b7117b97 | ||
|
|
0f4de329e5 | ||
|
|
9b290bea40 | ||
|
|
fd3c83cb13 | ||
|
|
ec137d2513 | ||
|
|
9da5bdaad4 | ||
|
|
eec1850712 | ||
|
|
802ab4c6c1 | ||
|
|
85d09dc48c | ||
|
|
1daa02af52 | ||
|
|
1729505bfe | ||
|
|
00617d5c64 | ||
|
|
35b8003cf9 | ||
|
|
56ed8a787a | ||
|
|
fd26de7619 | ||
|
|
205a2e10a5 | ||
|
|
8514cc3da7 | ||
|
|
8bc8df7625 | ||
|
|
7ffa15d2d7 | ||
|
|
80be0e403d | ||
|
|
ee2538ba7f | ||
|
|
6ca6ec28ac | ||
|
|
94203785f1 | ||
|
|
3f538d9b78 | ||
|
|
e6a0578884 | ||
|
|
e11e890818 | ||
|
|
3e7a48d27a | ||
|
|
eeba959ba5 | ||
|
|
e7fa1036be | ||
|
|
542a7e1141 | ||
|
|
5951f4438a | ||
|
|
1fbae6bd7b | ||
|
|
b73924aea8 | ||
|
|
005443f4ae | ||
|
|
abb55d4424 | ||
|
|
e0538da079 | ||
|
|
665bf5a034 | ||
|
|
dc7e1282c6 | ||
|
|
3a877d4f4a | ||
|
|
8a23c9a327 | ||
|
|
452c0edfc7 | ||
|
|
2b9307aa17 | ||
|
|
f91d5e1c29 | ||
|
|
2fbfd14252 | ||
|
|
c09dd92cff | ||
|
|
6b08074a70 | ||
|
|
9cb5971182 | ||
|
|
6f37d95c24 | ||
|
|
d290ba24b7 | ||
|
|
f57d23026b | ||
|
|
1a70ccff55 | ||
|
|
bd6a51e58d | ||
|
|
a9c122b144 | ||
|
|
ed56170809 | ||
|
|
a36e5fce29 | ||
|
|
760bfaf4d7 | ||
|
|
24463720b1 | ||
|
|
516470e8ae | ||
|
|
7f530d0476 | ||
|
|
8a2706d70b | ||
|
|
27f09480a0 | ||
|
|
c03dcf6d2e | ||
|
|
bdb2ae9c2f | ||
|
|
3413fe6943 | ||
|
|
e6ae9e8bd6 | ||
|
|
8d9426f257 | ||
|
|
30247e3def | ||
|
|
084dc32d2d | ||
|
|
3393f1397b | ||
|
|
61784bcfc4 | ||
|
|
bd692fc60c | ||
|
|
2c71110fa5 | ||
|
|
738299e8d3 | ||
|
|
c8b6dc27b2 | ||
|
|
1493aa39a3 | ||
|
|
f115031846 | ||
|
|
571b85dfd8 | ||
|
|
75cc9e9030 | ||
|
|
656a707b4c | ||
|
|
04afe7a934 | ||
|
|
689670b3ff | ||
|
|
6273a9decb | ||
|
|
72336d4f71 | ||
|
|
731e998eb2 | ||
|
|
9bf53114de | ||
|
|
0e1b8db688 | ||
|
|
3a62e2e6c0 | ||
|
|
08764cb3cb | ||
|
|
9c52545f63 | ||
|
|
a6c30d33d4 | ||
|
|
25974af229 | ||
|
|
607dfc9be3 | ||
|
|
560e669700 | ||
|
|
ba403c9360 | ||
|
|
0f1c9ff05d | ||
|
|
662f08e115 | ||
|
|
d647a32e9f | ||
|
|
375e72cb98 | ||
|
|
34c7cafdfe | ||
|
|
03e0eefe4d | ||
|
|
f41425f03d | ||
|
|
400b91278f | ||
|
|
9088f77ae5 | ||
|
|
86da3217d1 | ||
|
|
24908e52af | ||
|
|
1261a6790d | ||
|
|
59fa61864a | ||
|
|
1cbfe017ea | ||
|
|
f469369b14 | ||
|
|
1ddcaed483 | ||
|
|
7bb7736f18 | ||
|
|
d1e7e7a2a6 | ||
|
|
0c4b7b0586 | ||
|
|
f320f22863 | ||
|
|
d224cd99bb | ||
|
|
b955d31770 | ||
|
|
46786e32a3 | ||
|
|
eef449af49 | ||
|
|
b4eb8d56a6 | ||
|
|
c896ac72e8 | ||
|
|
b599cb33ff | ||
|
|
b3eab1a2a0 | ||
|
|
79d9dc7b24 | ||
|
|
7b573f8e6b | ||
|
|
7bd769e294 | ||
|
|
fde5f86313 | ||
|
|
3c23bf7ec9 | ||
|
|
4665f8b74e | ||
|
|
0c4c7489e9 | ||
|
|
5a43e677c5 | ||
|
|
38d4274ece | ||
|
|
743098d0b0 | ||
|
|
0e5221fa6e | ||
|
|
b458bde8a1 | ||
|
|
c663d10515 | ||
|
|
cec19c3db3 | ||
|
|
ff58539e2e | ||
|
|
d8e7689a94 | ||
|
|
32cfbb327c | ||
|
|
245e87256b | ||
|
|
ed8c69037f | ||
|
|
3f76d22d67 | ||
|
|
980988e684 | ||
|
|
347811abb6 | ||
|
|
ccb8b0c8e7 | ||
|
|
18137ab48e | ||
|
|
a9f435ae3d | ||
|
|
0758cfef64 | ||
|
|
06d1d56448 | ||
|
|
07c70eaccc | ||
|
|
5ad6413952 | ||
|
|
0b9d9ac7f2 | ||
|
|
3ab87027ab | ||
|
|
7545a774ba | ||
|
|
db16eb8e29 | ||
|
|
e5a27a7c6f | ||
|
|
d26bc102d1 | ||
|
|
fc6a8afd93 | ||
|
|
5a9d401446 | ||
|
|
77ac40b445 | ||
|
|
a29454f672 | ||
|
|
80ee7c8e54 | ||
|
|
fb202f80a5 | ||
|
|
2b2042807b | ||
|
|
45dbd5aa44 | ||
|
|
ee65251bf5 | ||
|
|
eaeb11f9ce | ||
|
|
2f74633abb | ||
|
|
0f346dc725 | ||
|
|
1b92848964 | ||
|
|
3d91583585 | ||
|
|
f76d9fa3e4 | ||
|
|
b00b2e406e | ||
|
|
74717e2b93 | ||
|
|
9b54ed6bc7 | ||
|
|
7b36c64b34 | ||
|
|
da09884136 | ||
|
|
64aaf37556 | ||
|
|
11104223eb | ||
|
|
0c119bc137 | ||
|
|
5c058e626b | ||
|
|
2005ae2bf3 | ||
|
|
d0650c7cf4 | ||
|
|
2df4e6480a | ||
|
|
017a1686dc | ||
|
|
279dc03695 | ||
|
|
c8c482f692 | ||
|
|
02a0e3ebcd | ||
|
|
fc0b3f3b38 | ||
|
|
2925900214 | ||
|
|
eae370e41c | ||
|
|
d0338a604a | ||
|
|
e22b98b476 | ||
|
|
4d838d290d | ||
|
|
048efdf59f | ||
|
|
65dbc6b8e5 | ||
|
|
627a00beb4 | ||
|
|
e00ed13ad1 | ||
|
|
af2adeba13 | ||
|
|
893fa6bd90 | ||
|
|
512188c8dd | ||
|
|
aae6761809 | ||
|
|
c3f055d0c4 | ||
|
|
93c6bec452 | ||
|
|
04d5df20d1 | ||
|
|
665eca0699 | ||
|
|
9a1534464f | ||
|
|
f856fc6fac | ||
|
|
4af8e73303 | ||
|
|
23239f1fec | ||
|
|
853e4d6fde | ||
|
|
14a37ad16e | ||
|
|
c944044465 | ||
|
|
8a63ca2310 | ||
|
|
12e5e3b35e | ||
|
|
553a85ef86 | ||
|
|
d604ff3c24 | ||
|
|
4f9eee7d46 | ||
|
|
fe673a94ed | ||
|
|
bcd891d653 | ||
|
|
1e75edf262 | ||
|
|
73478d6a81 | ||
|
|
982080a930 | ||
|
|
66cd5070dc | ||
|
|
c05b0eaa59 | ||
|
|
29a073d844 | ||
|
|
a0e69428e4 | ||
|
|
de7012cabf | ||
|
|
3f6a103915 | ||
|
|
734765dbdd | ||
|
|
117c4c5978 | ||
|
|
13e33a6614 | ||
|
|
affb495136 | ||
|
|
9b2dafd668 | ||
|
|
5f32c0401f | ||
|
|
f35f40ed27 | ||
|
|
46f0d3ef74 | ||
|
|
c27c785ac2 | ||
|
|
6e844e8c3b | ||
|
|
4186c36f30 | ||
|
|
2d727a0da8 | ||
|
|
39e574e9dc | ||
|
|
efb94cbd67 | ||
|
|
757e33dfb4 | ||
|
|
ab9bdf9f07 | ||
|
|
a68632a888 | ||
|
|
9b0dc8b413 | ||
|
|
f43fe5830e | ||
|
|
ce616b328c | ||
|
|
824a8ff97a | ||
|
|
ef9018b92f | ||
|
|
a088d10935 | ||
|
|
2e561697ac | ||
|
|
d242acd502 | ||
|
|
d37b44d3f6 | ||
|
|
6243fc88c9 | ||
|
|
f74e865b06 | ||
|
|
1a2bc02188 | ||
|
|
b0153e9f61 | ||
|
|
7f3fc1b88a | ||
|
|
40bf0c75bf | ||
|
|
9407bd205c | ||
|
|
3317c8dddc | ||
|
|
fc4b6eb1af | ||
|
|
2aaaf2f4a2 | ||
|
|
92aa96a644 | ||
|
|
6a0a4023ad | ||
|
|
8569610e52 | ||
|
|
51ffa4d469 | ||
|
|
e4c4d2bbf0 | ||
|
|
35d6f1fb34 | ||
|
|
edac5fda8c | ||
|
|
040d3e4433 | ||
|
|
011dd4c069 | ||
|
|
b4f93fc0a5 | ||
|
|
e0d74ba2a9 | ||
|
|
935826617e | ||
|
|
b9f2effb86 | ||
|
|
5c86de555a | ||
|
|
c4e7807d18 | ||
|
|
78dd0588ce | ||
|
|
2d73894880 | ||
|
|
8ac19e557b | ||
|
|
f677a75ad1 | ||
|
|
8e55a4d824 | ||
|
|
6b5d8ff0f1 | ||
|
|
0c7da53349 | ||
|
|
2a3cc11728 | ||
|
|
a806634bc0 | ||
|
|
6879d046f8 | ||
|
|
0248f84ca0 | ||
|
|
ff0706dae5 | ||
|
|
b73fe0398f | ||
|
|
f78ae4a818 | ||
|
|
072f6d8c69 | ||
|
|
68b68eb4c5 | ||
|
|
7f95eead50 | ||
|
|
1935de8f20 | ||
|
|
c1e9fde6e8 | ||
|
|
9ded9b84e0 | ||
|
|
21037529c0 | ||
|
|
ec8bdbe6e1 | ||
|
|
b37fd6f4ab | ||
|
|
d853bb2c62 | ||
|
|
68dcacb918 | ||
|
|
49a7408715 | ||
|
|
5abbddba1e | ||
|
|
2382bf1063 | ||
|
|
f9b409634b | ||
|
|
bb96383d27 | ||
|
|
96a7a46981 | ||
|
|
4b3446ce0e | ||
|
|
6f302e2536 | ||
|
|
53c326ad05 | ||
|
|
7befb88e15 | ||
|
|
68e8ccf6bd | ||
|
|
32e80c7e95 | ||
|
|
c07a3b9d0d | ||
|
|
893d1a881d | ||
|
|
43ef130052 | ||
|
|
d5bea0ca53 | ||
|
|
9c740c5cc1 | ||
|
|
a5b85c296a | ||
|
|
2d453bb553 | ||
|
|
ad1d247694 | ||
|
|
cf7535e2ba | ||
|
|
0077dc2f1c | ||
|
|
59a50e163f | ||
|
|
22e5b958bc | ||
|
|
905c4b362c | ||
|
|
6e71f20470 | ||
|
|
c326136bdb | ||
|
|
21c13835e6 | ||
|
|
deef3bd8b5 | ||
|
|
87afad29ce | ||
|
|
436233e735 | ||
|
|
6e367ddd74 | ||
|
|
fcdfaf5564 | ||
|
|
97884ae25b | ||
|
|
bc2ffef17e | ||
|
|
890c13d83a | ||
|
|
ec8d4b56b5 | ||
|
|
99cdfbc07b | ||
|
|
71efde08a8 | ||
|
|
678cef0a45 | ||
|
|
57f1a48602 | ||
|
|
f5eb1619ed | ||
|
|
032ed27c38 | ||
|
|
78f8407eca | ||
|
|
9a22001289 | ||
|
|
8e08b5003e | ||
|
|
e545c8b897 | ||
|
|
092a265e6d | ||
|
|
dc1f494e92 | ||
|
|
dff17fd11f | ||
|
|
7b702e98da | ||
|
|
17d07f3b14 | ||
|
|
a21087ac9b | ||
|
|
0af1eebd62 | ||
|
|
85af73df99 | ||
|
|
c7a97711c0 | ||
|
|
ffbe05b2ae | ||
|
|
39169d3afe | ||
|
|
c89ba12fb5 | ||
|
|
8fdec72f8d | ||
|
|
13119c95ac | ||
|
|
c70ecd9cfd | ||
|
|
6c43881cf4 | ||
|
|
14f5d5daa4 | ||
|
|
f342cd6b56 | ||
|
|
57929f62ad | ||
|
|
523ee1e2a9 | ||
|
|
8b0f221eef | ||
|
|
656405edbc | ||
|
|
407c04fe2c | ||
|
|
3866d126b7 | ||
|
|
bdf836b7d9 | ||
|
|
997de528b8 | ||
|
|
8faacab53a | ||
|
|
659c327a6d | ||
|
|
bcc2f531c3 | ||
|
|
020df5c1f7 | ||
|
|
0d8e4dee35 | ||
|
|
d6781e1d14 | ||
|
|
c313184666 | ||
|
|
ea3b43ba88 | ||
|
|
0eebddb24c | ||
|
|
d42cd59880 | ||
|
|
6eb859b9f1 | ||
|
|
19f1322246 | ||
|
|
38f45ad483 | ||
|
|
7f46ff6d72 | ||
|
|
be19c32fea | ||
|
|
8da0e98d23 | ||
|
|
73a2f05509 | ||
|
|
bb23f998e0 | ||
|
|
75915ff366 | ||
|
|
517e801580 | ||
|
|
12474e23f9 | ||
|
|
00bdd859a7 | ||
|
|
3a3af9ea00 | ||
|
|
b9244bd11a | ||
|
|
532ec0129a | ||
|
|
464e47d6ad | ||
|
|
2bbdd3f044 | ||
|
|
0757a31381 | ||
|
|
1803b1a2ee | ||
|
|
36634414bc | ||
|
|
802448cb5a | ||
|
|
8ab9b4d1c3 | ||
|
|
02fa33597a | ||
|
|
5edfda6c1a | ||
|
|
5a565a16fe | ||
|
|
652ef7ee51 | ||
|
|
fb47480fba | ||
|
|
fc3ec644b3 | ||
|
|
9f21f5900f | ||
|
|
11710d36d1 | ||
|
|
a975ab58ee | ||
|
|
1306882377 | ||
|
|
b555e24fef | ||
|
|
b8250d5e44 | ||
|
|
4175c84363 | ||
|
|
9917787d6f | ||
|
|
d189e09aba | ||
|
|
7ff20245b3 | ||
|
|
da1696a059 | ||
|
|
e6202103bc | ||
|
|
7c659371a9 | ||
|
|
83886362be | ||
|
|
70de4f750c | ||
|
|
af901baff3 | ||
|
|
d69f4bbcaf | ||
|
|
089e3dc209 | ||
|
|
c158c4e18e | ||
|
|
300d365d8b | ||
|
|
81df005655 | ||
|
|
feb19c4eb5 | ||
|
|
9360787897 | ||
|
|
ac79557e22 | ||
|
|
c9695b1d2f | ||
|
|
27177996d3 | ||
|
|
2306330fd0 | ||
|
|
0d1e85d0c2 | ||
|
|
3b5a305122 | ||
|
|
1840d7b50e | ||
|
|
37b69833b3 | ||
|
|
093f766d1d | ||
|
|
69d8459b1c | ||
|
|
fa8a526642 | ||
|
|
1d35d951e6 | ||
|
|
3c0420f42f | ||
|
|
d000a825d3 | ||
|
|
23b28672d4 | ||
|
|
a076c9f420 | ||
|
|
e1db294b07 | ||
|
|
2004c3a7d5 | ||
|
|
44ac5270e0 | ||
|
|
bdc7a8f5ed | ||
|
|
f9baa4a8ad | ||
|
|
9c94a273ea | ||
|
|
efffbab4a7 | ||
|
|
7b53468c2e | ||
|
|
59243be030 | ||
|
|
57c1d070d1 | ||
|
|
5b2f2b0fd7 | ||
|
|
bdcc3bb1f5 | ||
|
|
18d45aa1a3 | ||
|
|
b5bb8efe0a | ||
|
|
f18c18230b | ||
|
|
1a83d21410 | ||
|
|
094cebe674 | ||
|
|
3963099053 | ||
|
|
f3181cc0f1 | ||
|
|
2fd1e998f4 | ||
|
|
c5a1980e0d | ||
|
|
008863fee8 | ||
|
|
d470ca4b47 | ||
|
|
35f450e444 | ||
|
|
206fb4e584 | ||
|
|
62088b36a4 | ||
|
|
aa5fd530d3 | ||
|
|
f0ee64bafa | ||
|
|
dfa413da6f | ||
|
|
9eb5e699e1 | ||
|
|
1927008c2e | ||
|
|
5d0dac6947 | ||
|
|
71351ad701 | ||
|
|
abf5c8fb3c | ||
|
|
865311d864 | ||
|
|
4e8b8e0bc2 | ||
|
|
c0fbf846bd | ||
|
|
2d4c1b751e | ||
|
|
91b17ef4a2 | ||
|
|
9b748f7334 | ||
|
|
517f2bcee7 | ||
|
|
4607e5ba0f | ||
|
|
29c82177a9 | ||
|
|
69a0c779d9 | ||
|
|
2deaed2067 | ||
|
|
3be9def609 | ||
|
|
bd3d800cde | ||
|
|
cef2449d45 | ||
|
|
8d894c97f3 | ||
|
|
0e1b5b19d2 | ||
|
|
2595c11686 | ||
|
|
334e08730e | ||
|
|
d6ff996dbe | ||
|
|
3040b89a9e | ||
|
|
aed180c845 | ||
|
|
4ba1e2f661 | ||
|
|
8b3ef3a3f3 | ||
|
|
b293fee742 | ||
|
|
fb608ed30a | ||
|
|
2654de96ba | ||
|
|
8e43afe408 | ||
|
|
e2aea345d4 | ||
|
|
78295898cb | ||
|
|
4fcdd9f370 | ||
|
|
73df680214 | ||
|
|
fa4aa154a3 | ||
|
|
cf7cdbc41b | ||
|
|
c2561a1de0 | ||
|
|
a36abe0272 | ||
|
|
5b10d697f6 | ||
|
|
e0f07ccc3b | ||
|
|
938ea8fb73 | ||
|
|
ea6a338128 | ||
|
|
9c66f74a5b | ||
|
|
9dc3ad38fc | ||
|
|
9599cdd2f6 | ||
|
|
4e5becb647 | ||
|
|
1d15a64945 | ||
|
|
21891d2d9d | ||
|
|
5b066c9dde | ||
|
|
e4668cf938 | ||
|
|
3978ebc230 | ||
|
|
4402db33fd | ||
|
|
3511c02c69 | ||
|
|
12be24c050 | ||
|
|
ce3a668103 | ||
|
|
80db7f0b74 | ||
|
|
451b9fc0f1 | ||
|
|
e2ed7f0d77 | ||
|
|
4743f40154 | ||
|
|
53f127987c | ||
|
|
f6c6111459 | ||
|
|
f5dd1c39ce | ||
|
|
b519b53419 | ||
|
|
c5de765e52 | ||
|
|
1381a7d957 | ||
|
|
602a5eb2ab | ||
|
|
7d41318d15 | ||
|
|
dd8cb8dfd0 | ||
|
|
557c2b018a | ||
|
|
3add01d57e | ||
|
|
2ad1ea98f1 | ||
|
|
cfc317cf19 | ||
|
|
242704f853 | ||
|
|
6df56c2d77 | ||
|
|
49634a2f52 | ||
|
|
b7442fe445 | ||
|
|
3121532217 | ||
|
|
20ac12ca0d | ||
|
|
f0b222140e | ||
|
|
2a35ca6094 | ||
|
|
b9428e3898 | ||
|
|
93f9636916 | ||
|
|
6a8a6a08db | ||
|
|
2c24aba558 | ||
|
|
a35d7dc5ae | ||
|
|
1e9e7e4cd7 | ||
|
|
cfd97ebd3d | ||
|
|
3ac8dc5558 | ||
|
|
6f93440b11 | ||
|
|
9283f419ba | ||
|
|
be67b36b6a | ||
|
|
6934daecff | ||
|
|
3d74d027c1 | ||
|
|
e048235dad | ||
|
|
d21dff08b8 | ||
|
|
c4b03d1316 | ||
|
|
5d54298a22 | ||
|
|
11e9f1749a | ||
|
|
09105152e4 | ||
|
|
a2b8cfe512 | ||
|
|
f42f244443 | ||
|
|
b81aeaebd3 | ||
|
|
e0d93b0630 | ||
|
|
e4a2897731 | ||
|
|
5f06c4c3c0 | ||
|
|
b693b34fe7 | ||
|
|
b7f469957c | ||
|
|
4a7b415635 | ||
|
|
d9985d03ab | ||
|
|
eda72128da | ||
|
|
ebdc2dfb0e | ||
|
|
0eff85dca3 | ||
|
|
da5796b563 | ||
|
|
310d4e58bb | ||
|
|
455351e3a8 | ||
|
|
b4f2b82a0d | ||
|
|
0e7960fced | ||
|
|
6e324fd5ab | ||
|
|
167498dd2c | ||
|
|
688eaf6aab | ||
|
|
d7df105e04 | ||
|
|
ea5c4cd027 | ||
|
|
9589706df9 | ||
|
|
e6e37aec47 | ||
|
|
fc2d5fe00e | ||
|
|
a73d3d375a | ||
|
|
ce97c8f7d9 | ||
|
|
a48abc56dd | ||
|
|
1044d7a8d1 | ||
|
|
1d2584001f | ||
|
|
fb0a075c50 | ||
|
|
8a8c785a31 | ||
|
|
4e976fc4ec | ||
|
|
cdf06578c1 | ||
|
|
4a131d6215 | ||
|
|
45f71cdcc1 | ||
|
|
5192175ddc | ||
|
|
274a672637 | ||
|
|
c281ab5a39 | ||
|
|
0106afc93c | ||
|
|
d69b091858 | ||
|
|
f71274d90d | ||
|
|
a37b9a1036 | ||
|
|
a9b8174b10 | ||
|
|
20a0eda5db | ||
|
|
e231dba0b0 | ||
|
|
d9459dc8fa | ||
|
|
a55ff5ce5a | ||
|
|
f2ea1cde46 | ||
|
|
04dd8003f7 | ||
|
|
b82b46f7d7 | ||
|
|
33bccd10fe | ||
|
|
9956f1ae4f | ||
|
|
09a3c5da23 | ||
|
|
b1d51bbefb | ||
|
|
9e7aaa6c91 | ||
|
|
9686c97731 | ||
|
|
663718ece4 | ||
|
|
e3048d1eb2 | ||
|
|
c6785bfda0 | ||
|
|
781438d83a | ||
|
|
f5625cc6b9 | ||
|
|
132d375d81 | ||
|
|
54007ecd28 | ||
|
|
03c2b55776 | ||
|
|
c6e46384f8 | ||
|
|
ce1c607f05 | ||
|
|
19bbe51f01 | ||
|
|
ff4a4eaf78 | ||
|
|
2cf08f74db | ||
|
|
cc6bbc9869 | ||
|
|
8d962516f4 | ||
|
|
ced4ca01cb | ||
|
|
1195323acd | ||
|
|
87b62aef70 | ||
|
|
ec89ba0155 | ||
|
|
0695103589 | ||
|
|
8d47b09e80 | ||
|
|
3cd156ae42 | ||
|
|
4a22f62ec5 | ||
|
|
9ad37b4412 | ||
|
|
9fcabcb05e | ||
|
|
db1ae6020d | ||
|
|
7e039c9055 | ||
|
|
db7482ff12 | ||
|
|
d63ae7cadd | ||
|
|
c4fa0d405e | ||
|
|
63fef3ab7c | ||
|
|
7019c07a6d | ||
|
|
006ea9844a | ||
|
|
d2bbfe01f1 | ||
|
|
86d8ff3c68 | ||
|
|
00dacc32df | ||
|
|
1c1bd9265e | ||
|
|
0bb090eee6 | ||
|
|
d2f3bfb2e3 | ||
|
|
634ce0dddf | ||
|
|
c82bacb037 | ||
|
|
3edfd0892a | ||
|
|
30c0fd600f | ||
|
|
ccb31de1ba | ||
|
|
a74b623c10 | ||
|
|
5808e8f321 | ||
|
|
4f3fef3bfe | ||
|
|
0c07e649bf | ||
|
|
fc2820ec11 | ||
|
|
312fb033e0 | ||
|
|
18bc4dc739 | ||
|
|
2b61b27271 | ||
|
|
58c9f75b91 | ||
|
|
790f1fb8a3 | ||
|
|
5c4f3f7fe4 | ||
|
|
86ead09080 | ||
|
|
0932507346 | ||
|
|
21f7b7120a | ||
|
|
473135bfc5 | ||
|
|
ce7960e5e9 | ||
|
|
3be96cf035 | ||
|
|
17c440ee43 | ||
|
|
5d881ca154 | ||
|
|
d5216e3784 | ||
|
|
bd29c64370 | ||
|
|
a9f3ab259a | ||
|
|
318486d62b | ||
|
|
f5db5c39c3 | ||
|
|
7bc945b243 | ||
|
|
e4b29b3ff9 | ||
|
|
046aaa0649 | ||
|
|
f653c74ce8 | ||
|
|
0c73c55b9d | ||
|
|
8dec54e96f | ||
|
|
859ae966c8 | ||
|
|
5abf5d3367 | ||
|
|
0dc4e63b7a | ||
|
|
d31d302896 | ||
|
|
1be8760c00 | ||
|
|
32836d05d8 | ||
|
|
1a6b4ae795 | ||
|
|
95d7ca5264 | ||
|
|
317252e1dd | ||
|
|
d9044b2d03 | ||
|
|
b6ae4e2b41 | ||
|
|
fce31df121 | ||
|
|
d5c1d86313 | ||
|
|
46df41504c | ||
|
|
48e232e04e | ||
|
|
58ff7c9235 | ||
|
|
730d664b91 | ||
|
|
36634ecca1 | ||
|
|
10c03ff01a | ||
|
|
e85b9db118 | ||
|
|
f6b0a7c780 | ||
|
|
3e785a2555 | ||
|
|
1cbb825892 | ||
|
|
82efa8298d | ||
|
|
c2ba716916 | ||
|
|
161bc5f69d | ||
|
|
b17237eb6b | ||
|
|
f61497ffd9 | ||
|
|
7f3c46942d | ||
|
|
4771882f50 | ||
|
|
345a1379ae | ||
|
|
33ab7f4d95 | ||
|
|
2a97cb34d7 | ||
|
|
03cbd8410f | ||
|
|
3c54bdd003 | ||
|
|
ba0a94e525 | ||
|
|
b439e0c2c2 | ||
|
|
f9281850ad | ||
|
|
4d5d25834e | ||
|
|
9e706ea096 | ||
|
|
46fe2bb8ac | ||
|
|
6405523232 | ||
|
|
930819ffa2 | ||
|
|
400a2b14f7 | ||
|
|
a40322b2e7 | ||
|
|
878df24a64 | ||
|
|
6969f40fa0 | ||
|
|
11fc8b6642 | ||
|
|
4e4024c182 | ||
|
|
1d1931f721 | ||
|
|
ffad6a4ae6 | ||
|
|
fbc86f6d3b | ||
|
|
00e1aac984 | ||
|
|
830cc66933 | ||
|
|
8869bafe9e | ||
|
|
6ea98fa056 | ||
|
|
837fb91133 | ||
|
|
9d1c4bd660 | ||
|
|
3b357eb509 | ||
|
|
786914b1a6 |
@@ -5,15 +5,16 @@ charset = utf-8
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
insert_final_newline = false
|
insert_final_newline = true
|
||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
tab_width = 4
|
tab_width = 4
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
disabled_rules=no-wildcard-imports,no-unused-imports
|
disabled_rules = no-wildcard-imports, no-unused-imports
|
||||||
|
|
||||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||||
ij_continuation_indent_size = 4
|
ij_continuation_indent_size = 4
|
||||||
|
|
||||||
[{*.kt,*.kts}]
|
[{*.kt,*.kts}]
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
ij_kotlin_allow_trailing_comma = true
|
ij_kotlin_allow_trailing_comma = true
|
||||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
|||||||
|
ko_fi: xtimms
|
||||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
custom: ["https://yoomoney.ru/to/410012543938752"]
|
||||||
|
|||||||
29
.github/ISSUE_TEMPLATE.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (https://github.com/KotatsuApp/Kotatsu/releases/latest)
|
||||||
|
- If this is an issue with a parser, that I should be opening an issue in https://github.com/KotatsuApp/kotatsu-parsers
|
||||||
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||||
|
- I will fill out the title and the information in this template
|
||||||
|
|
||||||
|
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Device information
|
||||||
|
* Kotatsu version: ?
|
||||||
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
## Issue/Request
|
||||||
|
?
|
||||||
|
|
||||||
|
## Other details
|
||||||
|
Additional details and attachments.
|
||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ⚠️ Source issue
|
- name: ⚠️ Source issue
|
||||||
url: https://github.com/nv95/kotatsu-parsers/issues/new
|
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
|
||||||
about: Issues and requests for sources should be opened in the kotatsu-parsers repository instead
|
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead
|
||||||
66
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report a bug in Kotatsu
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: summary
|
||||||
|
attributes:
|
||||||
|
label: Brief summary
|
||||||
|
description: Please describe, what went wrong
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Please provide a way to reproduce this issue. Screenshots or videos can be very helpful
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. Issue here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: kotatsu-version
|
||||||
|
attributes:
|
||||||
|
label: Kotatsu version
|
||||||
|
description: You can find your Kotatsu version in **Settings → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "3.3"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "12.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "LG Nexus 5X"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
|
||||||
|
required: true
|
||||||
93
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
93
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -1,93 +0,0 @@
|
|||||||
name: 🐞 Issue report
|
|
||||||
description: Report an issue in Kotatsu
|
|
||||||
labels: [bug]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce-steps
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: Provide an example of the issue.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
3. Issue here
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: Explain what you should expect to happen.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This should happen..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual-behavior
|
|
||||||
attributes:
|
|
||||||
label: Actual behavior
|
|
||||||
description: Explain what actually happens.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This happened instead..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: kotatsu-version
|
|
||||||
attributes:
|
|
||||||
label: Kotatsu version
|
|
||||||
description: You can find your Kotatsu version in **Settings → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "3.2.2"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 12"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "LG Nexus 5X"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[3.2.2](https://github.com/nv95/Kotatsu/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
17
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
17
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: ⭐ Feature request
|
name: ⭐ Feature request
|
||||||
description: Suggest a feature to improve Kotatsu
|
description: Suggest a new idea how to improve Kotatsu
|
||||||
labels: [feature request]
|
labels: [feature request]
|
||||||
body:
|
body:
|
||||||
|
|
||||||
@@ -14,13 +14,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
id: acknowledgements
|
||||||
attributes:
|
attributes:
|
||||||
@@ -28,12 +21,4 @@ body:
|
|||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[3.2.2](https://github.com/nv95/Kotatsu/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
required: true
|
||||||
29
.github/workflows/issue_moderator.yml
vendored
Normal file
29
.github/workflows/issue_moderator.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Issue moderator
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
moderate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Moderate issues
|
||||||
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
auto-close-rules: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
|
"message": "The acknowledgment section was not removed."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*\\* (Kotatsu version|Android version|Device): \\?.*",
|
||||||
|
"message": "Requested information in the template was not filled out."
|
||||||
|
}
|
||||||
|
]
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,11 +6,17 @@
|
|||||||
/.idea/dictionaries
|
/.idea/dictionaries
|
||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
/.idea/misc.xml
|
||||||
|
/.idea/discord.xml
|
||||||
|
/.idea/compiler.xml
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
/.idea/kotlinScripting.xml
|
/.idea/kotlinScripting.xml
|
||||||
|
/.idea/kotlinc.xml
|
||||||
/.idea/deploymentTargetDropDown.xml
|
/.idea/deploymentTargetDropDown.xml
|
||||||
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
|
/.idea/render.experimental.xml
|
||||||
|
/.idea/inspectionProfiles/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
4
.idea/.gitignore
generated
vendored
Normal file
4
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
/migrations.xml
|
||||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="11" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
5
.idea/gradle.xml
generated
5
.idea/gradle.xml
generated
@@ -4,16 +4,15 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="Android Studio default JDK" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
287
.idea/icon.svg
generated
Normal file
287
.idea/icon.svg
generated
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
inkscape:export-ydpi="39.689999"
|
||||||
|
inkscape:export-xdpi="39.689999"
|
||||||
|
inkscape:export-filename="/home/admin/Documents/projects/graphics/k/icon4.png"
|
||||||
|
width="512mm"
|
||||||
|
height="512mm"
|
||||||
|
viewBox="0 0 512 512.00002"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
|
||||||
|
sodipodi:docname="icon4.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1266">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1256" />
|
||||||
|
<feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1258" />
|
||||||
|
<feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="3"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1260" />
|
||||||
|
<feOffset
|
||||||
|
dx="6"
|
||||||
|
dy="6"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1262" />
|
||||||
|
<feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1264" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1059">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1049" />
|
||||||
|
<feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1051" />
|
||||||
|
<feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="3"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1053" />
|
||||||
|
<feOffset
|
||||||
|
dx="6"
|
||||||
|
dy="6"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1055" />
|
||||||
|
<feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1057" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1071">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1061" />
|
||||||
|
<feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1063" />
|
||||||
|
<feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="3"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1065" />
|
||||||
|
<feOffset
|
||||||
|
dx="6"
|
||||||
|
dy="6"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1067" />
|
||||||
|
<feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1069" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#0d47a1"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.175"
|
||||||
|
inkscape:cx="-361.03654"
|
||||||
|
inkscape:cy="630.78782"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1600"
|
||||||
|
inkscape:window-height="838"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
fit-margin-top="20"
|
||||||
|
fit-margin-left="20"
|
||||||
|
fit-margin-right="20"
|
||||||
|
fit-margin-bottom="20" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Слой 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-51.12025,-104.74797)">
|
||||||
|
<g
|
||||||
|
id="g1028"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1030"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1032"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1034"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1036"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1038"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1040"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1042"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1044"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1046"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1048"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1050"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1052"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1054"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<g
|
||||||
|
id="g1056"
|
||||||
|
transform="matrix(6.9754464,0,0,6.9754464,32.42507,404.31391)" />
|
||||||
|
<path
|
||||||
|
id="path1128"
|
||||||
|
d="m 307.12025,310.74755 c -50.53732,0 -91.66608,44.85688 -91.66608,99.99965 0,55.14277 41.12954,99.99964 91.66608,99.99964 50.53653,0 91.66607,-44.85687 91.66607,-99.99964 0,-55.14277 -41.12875,-99.99965 -91.66607,-99.99965 z m -34.21238,78.72707 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33357 c 4.11951,2.06718 5.77966,7.06403 3.72889,11.18199 z m 58.33338,-24.99991 c -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52264,-0.27656 -3.72733,-0.8789 l -12.93901,-6.47811 -12.9398,6.47811 c -4.13436,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05078,-4.11796 -0.39063,-9.11482 3.72733,-11.182 l 16.66634,-8.33356 c 2.34375,-1.17187 5.11092,-1.17187 7.45466,0 l 16.66635,8.33356 c 4.11874,2.06718 5.77889,7.06404 3.72811,11.182 z m 54.60606,13.81792 c 4.11795,2.06718 5.7781,7.06403 3.72733,11.18199 -1.46484,2.91327 -4.41092,4.60623 -7.45466,4.60623 -1.25312,0 -2.52265,-0.27656 -3.72733,-0.8789 l -12.9398,-6.4781 -12.9398,6.4781 c -4.11795,2.06718 -9.11481,0.37421 -11.18199,-3.72733 -2.05077,-4.11796 -0.39062,-9.11481 3.72733,-11.18199 l 16.66635,-8.33357 c 2.34374,-1.17187 5.11092,-1.17187 7.45466,0 z"
|
||||||
|
style="fill:#ffffff;stroke-width:0.781247" />
|
||||||
|
<path
|
||||||
|
id="path1130"
|
||||||
|
d="m 415.36283,274.00237 c -3.48202,-6.90544 -6.92029,-13.41714 -10.20934,-19.37102 l -8.26716,-14.47964 c -6.79607,-11.51871 -12.25699,-19.90305 -14.78354,-23.66554 -0.7164,-43.32797 -19.12415,-53.79356 -21.25617,-54.86777 -3.20624,-1.5789 -7.09607,-0.97656 -9.6195,1.56249 -12.25621,12.25621 -20.23118,24.4632 -24.00695,30.89286 h -40.20141 c -3.77577,-6.42888 -11.75153,-18.63665 -24.00695,-30.89286 -2.52265,-2.53905 -6.39685,-3.14139 -9.6195,-1.56249 -2.13202,1.07421 -20.54055,11.5398 -21.25617,54.86777 -2.52655,3.76327 -7.98669,12.14605 -14.78276,23.66476 l -8.27341,14.49214 c -3.28983,5.95701 -6.73044,12.47105 -10.21403,19.3804 l -7.4445,15.32572 c -17.72572,38.05377 -34.30066,85.4286 -34.30066,129.72766 0,69.32085 58.26776,128.42064 60.75838,130.89485 0.91171,0.91172 2.03437,1.61171 3.25546,2.01796 1.07421,0.35859 26.78974,8.757 69.31928,8.757 2.21327,0 4.32967,-0.8789 5.89217,-2.4414 l 5.89216,-5.89216 h 9.76559 l 5.89217,5.89216 c 1.56249,1.5625 3.67811,2.4414 5.89216,2.4414 42.52954,0 68.24507,-8.39841 69.31929,-8.757 1.22109,-0.40703 2.34374,-1.10703 3.25546,-2.01796 2.48905,-2.47421 60.75681,-61.57322 60.75681,-130.89485 0,-44.29906 -16.57494,-91.67389 -34.30066,-129.72766 z M 348.7865,227.41426 c 4.60624,0 4.41171,7.35466 4.41171,11.96089 4.60623,0 12.25464,0.1 12.25464,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19528,0 -16.66634,-7.47028 -16.66634,-16.66634 0,-9.19606 7.47106,-16.66713 16.66634,-16.66713 z m -57.69823,30.14364 c 1.28593,-3.10858 4.32967,-5.14295 7.69841,-5.14295 h 16.66635 c 3.36952,0 6.41248,2.03437 7.69841,5.14295 1.28593,3.10858 0.56953,6.70545 -1.80703,9.082 l -8.33356,8.33356 c -1.62734,1.62734 -3.76014,2.4414 -5.89217,2.4414 -2.13202,0 -4.26404,-0.81406 -5.89216,-2.4414 l -8.33357,-8.33356 c -2.37421,-2.37655 -3.09061,-5.97342 -1.80468,-9.082 z m -25.63428,-30.14364 c 4.60623,0 4.4117,7.35466 4.4117,11.96089 4.60623,0 12.25465,0.1 12.25465,4.70624 0,9.19606 -7.47107,16.66634 -16.66635,16.66634 -9.19606,0 -16.66635,-7.47106 -16.66635,-16.66634 -7.8e-4,-9.19606 7.47029,-16.66713 16.66635,-16.66713 z m 41.66626,299.99893 c -59.7326,0 -108.33321,-52.34357 -108.33321,-116.66599 0,-64.32243 48.60061,-116.66599 108.33321,-116.66599 59.73259,0 108.3332,52.34356 108.3332,116.66599 0,64.32242 -48.60061,116.66599 -108.3332,116.66599 z"
|
||||||
|
style="fill:#ffffff;stroke-width:0.781247;filter:url(#filter1059)"
|
||||||
|
sodipodi:nodetypes="cccccccccccccccsccssccssccsccscsssssssssssccsscsscssssss" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1138"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)"
|
||||||
|
inkscape:groupmode="layer" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1140"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1142"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1144"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1146"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1148"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1150"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1152"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1154"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1156"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1158"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1160"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1162"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1164"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<g
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="g1166"
|
||||||
|
transform="matrix(0.78124721,0,0,0.78124721,107.12096,160.74809)" />
|
||||||
|
<path
|
||||||
|
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="m 344.3189,392.83707 c -4.60362,-2.75958 -5.36974,-9.69605 -1.45595,-13.18226 0.54459,-0.48508 5.34567,-3.07035 10.66909,-5.74503 7.5498,-3.79328 10.16725,-4.86303 11.89884,-4.86303 1.73503,0 4.42542,1.10391 12.3172,5.05396 11.72559,5.86898 12.60994,6.68326 12.60994,11.61118 0,3.40408 -0.99553,5.20819 -4.00363,7.25549 -3.08358,2.09867 -5.44113,1.68547 -13.60905,-2.38528 l -7.19926,-3.58796 -7.37198,3.59617 c -8.3911,4.09331 -10.26721,4.39753 -13.8552,2.24676 z"
|
||||||
|
id="path944" />
|
||||||
|
<path
|
||||||
|
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="m 285.98437,367.98056 c -3.86343,-2.35557 -5.1524,-8.06518 -2.66781,-11.81734 1.64304,-2.48125 20.719,-12.23981 23.92632,-12.23981 1.56364,0 4.61398,1.26582 12.2153,5.06905 8.53551,4.27064 10.3157,5.36752 11.30239,6.96403 1.75651,2.84207 1.95178,5.62136 0.58856,8.37633 -1.52635,3.08463 -3.36973,4.32306 -6.86644,4.61304 -2.68142,0.22236 -3.36743,-0.003 -10.22731,-3.35873 l -7.35311,-3.59707 -7.04119,3.52834 c -7.90523,3.96133 -10.62609,4.44409 -13.87671,2.46216 z"
|
||||||
|
id="path946" />
|
||||||
|
<path
|
||||||
|
style="fill:#ef5350;fill-opacity:1;stroke:none;stroke-width:5.18208;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="m 228.11707,393.18031 c -1.0244,-0.54435 -2.42484,-1.80721 -3.11209,-2.80633 -1.05812,-1.53828 -1.2181,-2.32693 -1.04433,-5.14815 0.29039,-4.71472 1.41139,-5.70783 12.90113,-11.42937 7.71258,-3.84061 9.99443,-4.74971 11.92193,-4.74971 1.94819,0 4.22735,0.92952 12.47354,5.08716 8.66324,4.3679 10.26522,5.3693 11.33052,7.08263 3.53608,5.68714 -0.55313,12.95355 -7.28968,12.95355 -1.25225,0 -4.29453,-1.20187 -9.08226,-3.58799 l -7.19927,-3.58796 -7.37197,3.59617 c -8.07507,3.93914 -10.21699,4.34922 -13.52752,2.59 z"
|
||||||
|
id="path948" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
11
.idea/inspectionProfiles/Project_Default.xml
generated
11
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,11 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Kotlin2JvmCompilerArguments">
|
|
||||||
<option name="jvmTarget" value="1.8" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/ktlint.xml
generated
6
.idea/ktlint.xml
generated
@@ -1,7 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KtlintProjectConfiguration">
|
<component name="KtlintProjectConfiguration">
|
||||||
|
<enableKtlint>false</enableKtlint>
|
||||||
<androidMode>true</androidMode>
|
<androidMode>true</androidMode>
|
||||||
<treatAsErrors>false</treatAsErrors>
|
<treatAsErrors>false</treatAsErrors>
|
||||||
|
<disabledRules>
|
||||||
|
<list>
|
||||||
|
<option value="no-empty-first-line-in-method-block" />
|
||||||
|
</list>
|
||||||
|
</disabledRules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
.idea/render.experimental.xml
generated
6
.idea/render.experimental.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RenderSettings">
|
|
||||||
<option name="quality" value="0.25" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
11
.travis.yml
11
.travis.yml
@@ -1,11 +0,0 @@
|
|||||||
language: android
|
|
||||||
dist: trusty
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- android-30
|
|
||||||
- build-tools-30.0.3
|
|
||||||
- platform-tools-30.0.5
|
|
||||||
- tools
|
|
||||||
before_install:
|
|
||||||
- yes | sdkmanager "platforms;android-30"
|
|
||||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug
|
|
||||||
3
.weblate
Normal file
3
.weblate
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[weblate]
|
||||||
|
url = https://hosted.weblate.org/api/
|
||||||
|
translation = kotatsu/strings
|
||||||
60
README.md
60
README.md
@@ -1,59 +1,53 @@
|
|||||||
# Kotatsu
|
# Kotatsu
|
||||||
|
|
||||||
Kotatsu is a free and open source manga reader for Android.
|
Kotatsu is a free and open source manga reader for Android.
|
||||||
|
|
||||||
  [](https://travis-ci.org/nv95/Kotatsu)  [](https://hosted.weblate.org/engage/kotatsu/) [](http://4pda.ru/forum/index.php?showtopic=697669) [](https://discord.gg/NNJ5RgVBC5)
|
   [](https://hosted.weblate.org/engage/kotatsu/) [](https://t.me/kotatsuapp) [](https://discord.gg/NNJ5RgVBC5)
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
- **Recommended:** Download and install APK from **[GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. Application has a built-in self-updating feature.
|
||||||
alt="Get it on F-Droid"
|
- Get it on **[F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu)**. The F-Droid build may be a bit outdated and some fixes might be missing.
|
||||||
height="80">](https://f-droid.org/packages/org.koitharu.kotatsu)
|
|
||||||
|
|
||||||
Download APK from Github Releases:
|
|
||||||
|
|
||||||
- [Latest release](https://github.com/nv95/Kotatsu/releases/latest)
|
|
||||||
- [Legacy build](https://github.com/nv95/Kotatsu/releases/tag/v0.4-legacy) (with Android 4.1+ support)
|
|
||||||
|
|
||||||
### Main Features
|
### Main Features
|
||||||
|
|
||||||
* Online manga catalogues
|
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
|
||||||
* Search manga by name and genre
|
* Search manga by name and genres
|
||||||
* Reading history
|
* Reading history and bookmarks
|
||||||
* Favourites organized by user-defined categories
|
* Favourites organized by user-defined categories
|
||||||
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
* Downloading manga and reading it offline. Third-party CBZ archives also supported
|
||||||
* Tablet-optimized material design UI
|
* Tablet-optimized Material You UI
|
||||||
* Standard and Webtoon-optimized reader
|
* Standard and Webtoon-optimized reader
|
||||||
* Notifications about new chapters with updates feed
|
* Notifications about new chapters with updates feed
|
||||||
* Available in multiple languages
|
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
|
||||||
* Password protect access to the app
|
* Password/fingerprint protect access to the app
|
||||||
|
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
|
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|
|
||||||
|  |  |
|
|  |  |
|
||||||
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
|
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
||||||
|
|
||||||
### Localization
|
### Localization
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/kotatsu/">
|
[<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
|
||||||
<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages, please head over to the Weblate <a href="https://hosted.weblate.org/engage/kotatsu/">project page</a>
|
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
||||||
|
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
||||||
|
|
||||||
### License
|
### License
|
||||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
|
||||||
|
|
||||||
Kotatsu is Free Software: You can use, study share and improve it at your
|
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
|
||||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
|
|
||||||
published by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
### Disclaimer
|
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
|
||||||
|
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
|
||||||
|
install instructions.
|
||||||
|
|
||||||
The developers of this application does not have any affiliation with the content providers available.
|
### DMCA disclaimer
|
||||||
|
|
||||||
|
The developers of this application does not have any affiliation with the content available in the app.
|
||||||
|
It is collecting from the sources freely available through any web browser.
|
||||||
|
|||||||
147
app/build.gradle
147
app/build.gradle
@@ -3,19 +3,22 @@ plugins {
|
|||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdk = 33
|
||||||
buildToolsVersion '32.0.0'
|
buildToolsVersion = '33.0.2'
|
||||||
namespace 'org.koitharu.kotatsu'
|
namespace = 'org.koitharu.kotatsu'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 32
|
//TODO: update as soon as sources becomes available
|
||||||
versionCode 406
|
//noinspection OldTargetApi
|
||||||
versionName '3.2.2'
|
targetSdkVersion 33
|
||||||
|
versionCode 566
|
||||||
|
versionName '5.3.9'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -41,79 +44,119 @@ android {
|
|||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
|
||||||
|
main.java.srcDirs += 'src/main/kotlin/'
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
freeCompilerArgs += [
|
freeCompilerArgs += [
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
'-opt-in=kotlin.ExperimentalStdlibApi',
|
||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||||
|
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||||
|
'-opt-in=coil.annotation.ExperimentalCoilApi',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError false
|
abortOnError true
|
||||||
disable 'MissingTranslation', 'PrivateResource'
|
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources = true
|
unitTests.includeAndroidResources true
|
||||||
unitTests.returnDefaultValues = false
|
unitTests.returnDefaultValues false
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterEvaluate {
|
||||||
|
compileDebugKotlin {
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
//noinspection GradleDependency
|
||||||
implementation('com.github.nv95:kotatsu-parsers:b495e5e457') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:42cc0430f8') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.6.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
|
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.4.2'
|
// TODO https://issuetracker.google.com/issues/254846063
|
||||||
implementation 'androidx.room:room-ktx:2.4.2'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
kapt 'androidx.room:room-compiler:2.4.2'
|
//noinspection GradleDependency
|
||||||
|
implementation('com.google.guava:guava:32.0.1-android') {
|
||||||
|
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||||
|
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||||
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
|
}
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
implementation 'androidx.room:room-runtime:2.5.2'
|
||||||
implementation 'com.squareup.okio:okio:3.0.0'
|
implementation 'androidx.room:room-ktx:2.5.2'
|
||||||
|
//noinspection KaptUsageInsteadOfKsp
|
||||||
|
kapt 'androidx.room:room-compiler:2.5.2'
|
||||||
|
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
|
implementation 'com.squareup.okio:okio:3.4.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||||
|
|
||||||
implementation 'io.insert-koin:koin-android:3.1.6'
|
implementation 'com.google.dagger:hilt-android:2.47'
|
||||||
implementation 'io.coil-kt:coil-base:1.4.0'
|
kapt 'com.google.dagger:hilt-compiler:2.47'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67'
|
||||||
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
|
implementation 'ch.acra:acra-http:5.11.0'
|
||||||
|
implementation 'ch.acra:acra-dialog:5.11.0'
|
||||||
|
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
|
testImplementation 'org.json:json:20230618'
|
||||||
testImplementation 'io.insert-koin:koin-test-junit4:3.1.5'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.4.2'
|
|
||||||
}
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
||||||
|
|
||||||
|
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
||||||
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||||
|
|
||||||
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.47'
|
||||||
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.47'
|
||||||
|
}
|
||||||
|
|||||||
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@@ -8,6 +8,13 @@
|
|||||||
public static void checkParameterIsNotNull(...);
|
public static void checkParameterIsNotNull(...);
|
||||||
public static void checkNotNullParameter(...);
|
public static void checkNotNullParameter(...);
|
||||||
}
|
}
|
||||||
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
|
-keep public class ** extends org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
-keep class org.koitharu.kotatsu.core.db.entity.* { *; }
|
||||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
-dontwarn okhttp3.internal.platform.**
|
||||||
|
-dontwarn org.conscrypt.**
|
||||||
|
-dontwarn org.bouncycastle.**
|
||||||
|
-dontwarn org.openjsse.**
|
||||||
|
|
||||||
|
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||||
|
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||||
|
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||||
|
|||||||
9
app/src/androidTest/assets/categories/simple.json
Normal file
9
app/src/androidTest/assets/categories/simple.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Read later",
|
||||||
|
"sortKey": 1,
|
||||||
|
"order": "NEWEST",
|
||||||
|
"createdAt": 1335906000000,
|
||||||
|
"isTrackingEnabled": true,
|
||||||
|
"isVisibleInLibrary": true
|
||||||
|
}
|
||||||
BIN
app/src/androidTest/assets/kotatsu_test.bak
Executable file
BIN
app/src/androidTest/assets/kotatsu_test.bak
Executable file
Binary file not shown.
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 1552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
36
app/src/androidTest/assets/manga/empty.json
Normal file
36
app/src/androidTest/assets/manga/empty.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
163
app/src/androidTest/assets/manga/full.json
Normal file
163
app/src/androidTest/assets/manga/full.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
35
app/src/androidTest/assets/manga/header.json
Normal file
35
app/src/androidTest/assets/manga/header.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": null,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
|
||||||
|
waitForIdle { cont.resume(Unit) }
|
||||||
|
}
|
||||||
54
app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt
Normal file
54
app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.squareup.moshi.*
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
object SampleData {
|
||||||
|
|
||||||
|
private val moshi = Moshi.Builder()
|
||||||
|
.add(DateAdapter())
|
||||||
|
.add(KotlinJsonAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val manga: Manga = loadAsset("manga/header.json", Manga::class)
|
||||||
|
|
||||||
|
val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class)
|
||||||
|
|
||||||
|
val tag = mangaDetails.tags.elementAt(2)
|
||||||
|
|
||||||
|
val chapter = checkNotNull(mangaDetails.chapters)[2]
|
||||||
|
|
||||||
|
val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class)
|
||||||
|
|
||||||
|
fun <T : Any> loadAsset(name: String, cls: KClass<T>): T {
|
||||||
|
val assets = InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
return assets.open(name).use {
|
||||||
|
moshi.adapter(cls.java).fromJson(it.source().buffer())
|
||||||
|
} ?: throw RuntimeException("Cannot read asset from json \"$name\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DateAdapter : JsonAdapter<Date>() {
|
||||||
|
|
||||||
|
@FromJson
|
||||||
|
override fun fromJson(reader: JsonReader): Date? {
|
||||||
|
val ms = reader.nextLong()
|
||||||
|
return if (ms == 0L) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Date(ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToJson
|
||||||
|
override fun toJson(writer: JsonWriter, value: Date?) {
|
||||||
|
writer.value(value?.time ?: 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MangaDatabaseTest {
|
class MangaDatabaseTest {
|
||||||
@@ -16,40 +14,44 @@ class MangaDatabaseTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||||
InstrumentationRegistry.getInstrumentation(),
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
MangaDatabase::class.java.canonicalName,
|
MangaDatabase::class.java,
|
||||||
FrameworkSQLiteOpenHelperFactory()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val migrations = databaseMigrations
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(IOException::class)
|
fun versions() {
|
||||||
fun migrateAll() {
|
assertEquals(1, migrations.first().startVersion)
|
||||||
helper.createDatabase(TEST_DB, 1).apply {
|
repeat(migrations.size) { i ->
|
||||||
// TODO execSQL("")
|
assertEquals(i + 1, migrations[i].startVersion)
|
||||||
close()
|
assertEquals(i + 2, migrations[i].endVersion)
|
||||||
}
|
}
|
||||||
|
assertEquals(DATABASE_VERSION, migrations.last().endVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateAll() {
|
||||||
|
helper.createDatabase(TEST_DB, 1).close()
|
||||||
for (migration in migrations) {
|
for (migration in migrations) {
|
||||||
helper.runMigrationsAndValidate(
|
helper.runMigrationsAndValidate(
|
||||||
TEST_DB,
|
TEST_DB,
|
||||||
migration.endVersion,
|
migration.endVersion,
|
||||||
true,
|
true,
|
||||||
migration
|
migration,
|
||||||
)
|
).close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prePopulate() {
|
||||||
|
val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources
|
||||||
|
helper.createDatabase(TEST_DB, DATABASE_VERSION).use {
|
||||||
|
DatabasePrePopulateCallback(resources).onCreate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
||||||
const val TEST_DB = "test-db"
|
const val TEST_DB = "test-db"
|
||||||
|
|
||||||
val migrations = arrayOf(
|
|
||||||
Migration1To2(),
|
|
||||||
Migration2To3(),
|
|
||||||
Migration3To4(),
|
|
||||||
Migration4To5(),
|
|
||||||
Migration5To6(),
|
|
||||||
Migration6To7(),
|
|
||||||
Migration7To8(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.koitharu.kotatsu.core.os
|
||||||
|
|
||||||
|
import android.content.pm.ShortcutInfo
|
||||||
|
import android.content.pm.ShortcutManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koitharu.kotatsu.SampleData
|
||||||
|
import org.koitharu.kotatsu.awaitForIdle
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AppShortcutManagerTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appShortcutManager: AppShortcutManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var database: MangaDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
|
database.clearAllTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUpdateShortcuts() = runTest {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
||||||
|
return@runTest
|
||||||
|
}
|
||||||
|
awaitUpdate()
|
||||||
|
assertTrue(getShortcuts().isEmpty())
|
||||||
|
historyRepository.addOrUpdate(
|
||||||
|
manga = SampleData.manga,
|
||||||
|
chapterId = SampleData.chapter.id,
|
||||||
|
page = 4,
|
||||||
|
scroll = 2,
|
||||||
|
percent = 0.3f,
|
||||||
|
)
|
||||||
|
awaitUpdate()
|
||||||
|
|
||||||
|
val shortcuts = getShortcuts()
|
||||||
|
assertEquals(1, shortcuts.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShortcuts(): List<ShortcutInfo> {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val manager = checkNotNull(context.getSystemService<ShortcutManager>())
|
||||||
|
return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitUpdate() {
|
||||||
|
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||||
|
instrumentation.awaitForIdle()
|
||||||
|
appShortcutManager.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.backup
|
||||||
|
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koitharu.kotatsu.SampleData
|
||||||
|
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AppBackupAgentTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var favouritesRepository: FavouritesRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var backupRepository: BackupRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var database: MangaDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
|
database.clearAllTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun backupAndRestore() = runTest {
|
||||||
|
val category = favouritesRepository.createCategory(
|
||||||
|
title = SampleData.favouriteCategory.title,
|
||||||
|
sortOrder = SampleData.favouriteCategory.order,
|
||||||
|
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
|
||||||
|
isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary,
|
||||||
|
)
|
||||||
|
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
|
||||||
|
historyRepository.addOrUpdate(
|
||||||
|
manga = SampleData.mangaDetails,
|
||||||
|
chapterId = SampleData.mangaDetails.chapters!![2].id,
|
||||||
|
page = 3,
|
||||||
|
scroll = 40,
|
||||||
|
percent = 0.2f,
|
||||||
|
)
|
||||||
|
val history = checkNotNull(historyRepository.getOne(SampleData.manga))
|
||||||
|
|
||||||
|
val agent = AppBackupAgent()
|
||||||
|
val backup = agent.createBackupFile(
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
repository = backupRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
database.clearAllTables()
|
||||||
|
assertTrue(favouritesRepository.getAllManga().isEmpty())
|
||||||
|
assertNull(historyRepository.getLastOrNull())
|
||||||
|
|
||||||
|
backup.inputStream().use {
|
||||||
|
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(category, favouritesRepository.getCategory(category.id))
|
||||||
|
assertEquals(history, historyRepository.getOne(SampleData.manga))
|
||||||
|
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
||||||
|
|
||||||
|
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
|
||||||
|
assertTrue(SampleData.tag in allTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun restoreOldBackup() {
|
||||||
|
val agent = AppBackupAgent()
|
||||||
|
val backup = File.createTempFile("backup_", ".tmp")
|
||||||
|
InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
.open("kotatsu_test.bak", AssetManager.ACCESS_STREAMING)
|
||||||
|
.use { input ->
|
||||||
|
backup.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backup.inputStream().use {
|
||||||
|
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||||
|
}
|
||||||
|
runTest {
|
||||||
|
assertEquals(6, historyRepository.observeAll().first().size)
|
||||||
|
assertEquals(2, favouritesRepository.observeCategories().first().size)
|
||||||
|
assertEquals(15, favouritesRepository.getAllManga().size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.domain
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import junit.framework.TestCase.*
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.koitharu.kotatsu.SampleData
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class TrackerTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var repository: TrackingRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataRepository: MangaDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tracker: Tracker
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun noUpdates() = runTest {
|
||||||
|
val manga = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(manga.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(manga, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||||
|
tracker.checkUpdates(manga, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(manga.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun hasUpdates() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badIds() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaBad = loadManga("bad_ids.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun badIds2() = runTest {
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaBad = loadManga("bad_ids.json")
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
tracker.deleteTrack(mangaFirst.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaBad, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fullReset() = runTest {
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
val mangaEmpty = loadManga("empty.json")
|
||||||
|
tracker.deleteTrack(mangaFull.id)
|
||||||
|
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
tracker.checkUpdates(mangaEmpty, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFull.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun syncWithHistory() = runTest {
|
||||||
|
val mangaFull = loadManga("full.json")
|
||||||
|
val mangaFirst = loadManga("first_chapters.json")
|
||||||
|
tracker.deleteTrack(mangaFull.id)
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFirst, commit = true).apply {
|
||||||
|
assertFalse(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assertEquals(3, newChapters.size)
|
||||||
|
}
|
||||||
|
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
|
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
|
||||||
|
repository.syncWithHistory(mangaFull, chapter.id)
|
||||||
|
|
||||||
|
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
|
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
|
||||||
|
repository.syncWithHistory(mangaFull, chapter.id)
|
||||||
|
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
|
||||||
|
tracker.checkUpdates(mangaFull, commit = true).apply {
|
||||||
|
assertTrue(isValid)
|
||||||
|
assert(newChapters.isEmpty())
|
||||||
|
}
|
||||||
|
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadManga(name: String): Manga {
|
||||||
|
val manga = SampleData.loadAsset("manga/$name", Manga::class)
|
||||||
|
dataRepository.storeManga(manga)
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.Buffer
|
||||||
|
import org.koitharu.kotatsu.core.network.CommonHeaders.ACCEPT_ENCODING
|
||||||
|
|
||||||
|
class CurlLoggingInterceptor(
|
||||||
|
private val curlOptions: String? = null
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
var isCompressed = false
|
||||||
|
|
||||||
|
val curlCmd = StringBuilder()
|
||||||
|
curlCmd.append("curl")
|
||||||
|
if (curlOptions != null) {
|
||||||
|
curlCmd.append(' ').append(curlOptions)
|
||||||
|
}
|
||||||
|
curlCmd.append(" -X ").append(request.method)
|
||||||
|
|
||||||
|
for ((name, value) in request.headers) {
|
||||||
|
if (name.equals(ACCEPT_ENCODING, ignoreCase = true) && value.equals("gzip", ignoreCase = true)) {
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
curlCmd.append(" -H \"").append(name).append(": ").append(value.escape()).append('\"')
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = request.body
|
||||||
|
if (body != null) {
|
||||||
|
val buffer = Buffer()
|
||||||
|
body.writeTo(buffer)
|
||||||
|
val charset = body.contentType()?.charset() ?: Charsets.UTF_8
|
||||||
|
curlCmd.append(" --data-raw '")
|
||||||
|
.append(buffer.readString(charset).replace("\n", "\\n"))
|
||||||
|
.append("'")
|
||||||
|
}
|
||||||
|
if (isCompressed) {
|
||||||
|
curlCmd.append(" --compressed")
|
||||||
|
}
|
||||||
|
curlCmd.append(" \"").append(request.url).append('"')
|
||||||
|
|
||||||
|
log("---cURL (" + request.url + ")")
|
||||||
|
log(curlCmd.toString())
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.escape() = replace("\"", "\\\"")
|
||||||
|
|
||||||
|
private fun log(msg: String) {
|
||||||
|
Log.d("CURL", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This parser is just for parser development, it should not be used in releases
|
* This parser is just for parser development, it should not be used in releases
|
||||||
*/
|
*/
|
||||||
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("", null)
|
get() = ConfigKey.Domain("")
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
@@ -25,7 +30,7 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS
|
|||||||
offset: Int,
|
offset: Int,
|
||||||
query: String?,
|
query: String?,
|
||||||
tags: Set<MangaTag>?,
|
tags: Set<MangaTag>?,
|
||||||
sortOrder: SortOrder?
|
sortOrder: SortOrder,
|
||||||
): List<Manga> {
|
): List<Manga> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
@@ -37,4 +42,4 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS
|
|||||||
override suspend fun getTags(): Set<MangaTag> {
|
override suspend fun getTags(): Set<MangaTag> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||||
|
|
||||||
|
class LoggingAdapterDataObserver(
|
||||||
|
private val tag: String,
|
||||||
|
) : AdapterDataObserver() {
|
||||||
|
|
||||||
|
override fun onChanged() {
|
||||||
|
Log.d(tag, "onChanged()")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
|
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
|
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateRestorationPolicyChanged() {
|
||||||
|
Log.d(tag, "onStateRestorationPolicyChanged()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
fun Throwable.printStackTraceDebug() = printStackTrace()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
6
app/src/debug/res/values/constants.xml
Normal file
6
app/src/debug/res/values/constants.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="account_type_sync" translatable="false">org.kotatsu.debug.sync</string>
|
||||||
|
<string name="sync_authority_history" translatable="false">org.koitharu.kotatsu.debug.history</string>
|
||||||
|
<string name="sync_authority_favourites" translatable="false">org.koitharu.kotatsu.debug.favourites</string>
|
||||||
|
</resources>
|
||||||
@@ -9,17 +9,46 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
|
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
|
||||||
android:dataExtractionRules="@xml/backup_rules"
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_content"
|
android:fullBackupContent="@xml/backup_content"
|
||||||
android:fullBackupOnly="true"
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:localeConfig="@xml/locales"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Kotatsu"
|
android:theme="@style/Theme.Kotatsu"
|
||||||
@@ -53,27 +82,57 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
<activity android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||||
android:label="@string/search_manga" />
|
android:label="@string/search_manga" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
||||||
|
android:label="@string/history" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity"
|
||||||
|
android:label="@string/updates" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
|
||||||
|
android:label="@string/favourites" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity"
|
||||||
|
android:label="@string/bookmarks" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
||||||
|
android:label="@string/suggestions" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||||
android:label="@string/settings" />
|
android:exported="true"
|
||||||
|
android:label="@string/settings">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="kotatsu" />
|
||||||
|
<data android:host="about" />
|
||||||
|
<data android:host="sync-settings" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
|
||||||
|
android:label="@string/local_manga_directories" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
||||||
android:label="@string/error_occurred"
|
android:label="@string/favourites"
|
||||||
android:theme="@android:style/Theme.DeviceDefault"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity"
|
|
||||||
android:label="@string/favourites_categories"
|
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
||||||
@@ -84,7 +143,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
android:name="org.koitharu.kotatsu.main.ui.protect.ProtectActivity"
|
||||||
@@ -94,14 +153,40 @@
|
|||||||
android:name="org.koitharu.kotatsu.settings.protect.ProtectSetupActivity"
|
android:name="org.koitharu.kotatsu.settings.protect.ProtectSetupActivity"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
|
android:name="org.koitharu.kotatsu.download.ui.list.DownloadsActivity"
|
||||||
android:launchMode="singleTop"
|
android:label="@string/downloads"
|
||||||
android:label="@string/downloads" />
|
android:launchMode="singleTop" />
|
||||||
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity"/>
|
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
|
||||||
|
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
||||||
|
android:label="@string/sync" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
||||||
|
android:label="@string/color_correction" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/settings"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="kotatsu" />
|
||||||
|
<data android:host="shikimori-auth" />
|
||||||
|
<data android:host="anilist-auth" />
|
||||||
|
<data android:host="mal-auth" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
|
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||||
@@ -109,6 +194,42 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthenticatorService"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
|
android:resource="@xml/authenticator_sync" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/favourites">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/sync_favourites" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/history">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/sync_history" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||||
@@ -123,6 +244,28 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/filepaths" />
|
android:resource="@xml/filepaths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncProvider"
|
||||||
|
android:authorities="@string/sync_authority_favourites"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/favourites"
|
||||||
|
android:syncable="true" />
|
||||||
|
<provider
|
||||||
|
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncProvider"
|
||||||
|
android:authorities="@string/sync_authority_history"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/history"
|
||||||
|
android:syncable="true" />
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="remove">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
|
||||||
@@ -153,7 +296,10 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.webkit.WebView.MetricsOptOut"
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||||
|
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package org.koitharu.kotatsu
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.StrictMode
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koitharu.kotatsu.core.db.databaseModule
|
|
||||||
import org.koitharu.kotatsu.core.github.githubModule
|
|
||||||
import org.koitharu.kotatsu.core.network.networkModule
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.ui.AppCrashHandler
|
|
||||||
import org.koitharu.kotatsu.core.ui.uiModule
|
|
||||||
import org.koitharu.kotatsu.details.detailsModule
|
|
||||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
|
||||||
import org.koitharu.kotatsu.history.historyModule
|
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.local.localModule
|
|
||||||
import org.koitharu.kotatsu.main.mainModule
|
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.reader.readerModule
|
|
||||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
|
||||||
import org.koitharu.kotatsu.search.searchModule
|
|
||||||
import org.koitharu.kotatsu.settings.settingsModule
|
|
||||||
import org.koitharu.kotatsu.suggestions.suggestionsModule
|
|
||||||
import org.koitharu.kotatsu.tracker.trackerModule
|
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
|
||||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
|
||||||
|
|
||||||
class KotatsuApp : Application() {
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
enableStrictMode()
|
|
||||||
}
|
|
||||||
initKoin()
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
|
||||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
|
||||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
|
||||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
|
||||||
widgetUpdater.subscribeToFavourites(get())
|
|
||||||
widgetUpdater.subscribeToHistory(get())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initKoin() {
|
|
||||||
startKoin {
|
|
||||||
androidContext(this@KotatsuApp)
|
|
||||||
modules(
|
|
||||||
networkModule,
|
|
||||||
databaseModule,
|
|
||||||
githubModule,
|
|
||||||
uiModule,
|
|
||||||
mainModule,
|
|
||||||
searchModule,
|
|
||||||
localModule,
|
|
||||||
favouritesModule,
|
|
||||||
historyModule,
|
|
||||||
remoteListModule,
|
|
||||||
detailsModule,
|
|
||||||
trackerModule,
|
|
||||||
settingsModule,
|
|
||||||
readerModule,
|
|
||||||
appWidgetModule,
|
|
||||||
suggestionsModule,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableStrictMode() {
|
|
||||||
StrictMode.setThreadPolicy(
|
|
||||||
StrictMode.ThreadPolicy.Builder()
|
|
||||||
.detectAll()
|
|
||||||
.penaltyLog()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
StrictMode.setVmPolicy(
|
|
||||||
StrictMode.VmPolicy.Builder()
|
|
||||||
.detectAll()
|
|
||||||
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
|
|
||||||
.setClassInstanceLimit(PagesCache::class.java, 1)
|
|
||||||
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
|
|
||||||
.penaltyLog()
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
|
||||||
.penaltyDeath()
|
|
||||||
.detectFragmentReuse()
|
|
||||||
.detectWrongFragmentContainer()
|
|
||||||
.detectRetainInstanceUsage()
|
|
||||||
.detectSetUserVisibleHint()
|
|
||||||
.detectFragmentTagUsage()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
|
|
||||||
class MangaDataRepository(private val db: MangaDatabase) {
|
|
||||||
|
|
||||||
suspend fun savePreferences(manga: Manga, mode: ReaderMode) {
|
|
||||||
val tags = manga.tags.toEntities()
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga.toEntity(), tags)
|
|
||||||
db.preferencesDao.upsert(
|
|
||||||
MangaPrefsEntity(
|
|
||||||
mangaId = manga.id,
|
|
||||||
mode = mode.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getReaderMode(mangaId: Long): ReaderMode? {
|
|
||||||
return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findMangaById(mangaId: Long): Manga? {
|
|
||||||
return db.mangaDao.find(mangaId)?.toManga()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
|
||||||
intent.manga != null -> intent.manga
|
|
||||||
intent.mangaId != 0L -> findMangaById(intent.mangaId)
|
|
||||||
else -> null // TODO resolve uri
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun storeManga(manga: Manga) {
|
|
||||||
val tags = manga.tags.toEntities()
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga.toEntity(), tags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findTags(source: MangaSource): Set<MangaTag> {
|
|
||||||
return db.tagsDao.findTags(source.name).toMangaTags()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
class MangaIntent private constructor(
|
|
||||||
val manga: Manga?,
|
|
||||||
val mangaId: Long,
|
|
||||||
val uri: Uri?,
|
|
||||||
) {
|
|
||||||
|
|
||||||
constructor(intent: Intent?) : this(
|
|
||||||
manga = intent?.getParcelableExtra<ParcelableManga>(KEY_MANGA)?.manga,
|
|
||||||
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
|
|
||||||
uri = intent?.data
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(args: Bundle?) : this(
|
|
||||||
manga = args?.getParcelable<ParcelableManga>(KEY_MANGA)?.manga,
|
|
||||||
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
|
|
||||||
uri = null
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val ID_NONE = 0L
|
|
||||||
|
|
||||||
const val KEY_MANGA = "manga"
|
|
||||||
const val KEY_ID = "id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Size
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.get
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.medianOrNull
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
object MangaUtils : KoinComponent {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatic determine type of manga by page size
|
|
||||||
* @return ReaderMode.WEBTOON if page is wide
|
|
||||||
*/
|
|
||||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
|
|
||||||
try {
|
|
||||||
val page = pages.medianOrNull() ?: return null
|
|
||||||
val url = MangaRepository(page.source).getPageUrl(page)
|
|
||||||
val uri = Uri.parse(url)
|
|
||||||
val size = if (uri.scheme == "cbz") {
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
val zip = ZipFile(uri.schemeSpecificPart)
|
|
||||||
val entry = zip.getEntry(uri.fragment)
|
|
||||||
zip.getInputStream(entry).use {
|
|
||||||
getBitmapSize(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.get()
|
|
||||||
.header(CommonHeaders.REFERER, page.referer)
|
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
|
||||||
.build()
|
|
||||||
get<OkHttpClient>().newCall(request).await().use {
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
getBitmapSize(it.body?.byteStream())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return size.width * 2 < size.height
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
|
|
||||||
val options = BitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
}
|
|
||||||
BitmapFactory.decodeFile(file.path, options)?.recycle()
|
|
||||||
options.outMimeType
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBitmapSize(input: InputStream?): Size {
|
|
||||||
val options = BitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
}
|
|
||||||
BitmapFactory.decodeStream(input, null, options)?.recycle()
|
|
||||||
val imageHeight: Int = options.outHeight
|
|
||||||
val imageWidth: Int = options.outWidth
|
|
||||||
check(imageHeight > 0 && imageWidth > 0)
|
|
||||||
return Size(imageWidth, imageHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
|
|
||||||
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val binding = onInflateView(layoutInflater, null)
|
|
||||||
viewBinding = binding
|
|
||||||
return MaterialAlertDialogBuilder(requireContext(), theme)
|
|
||||||
.setView(binding.root)
|
|
||||||
.also(::onBuildDialog)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
) = viewBinding?.root
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onBuildDialog(builder: MaterialAlertDialogBuilder) = Unit
|
|
||||||
|
|
||||||
protected fun bindingOrNull(): B? = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams
|
|
||||||
import androidx.appcompat.app.AppCompatDialog
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
protected val behavior: BottomSheetBehavior<*>?
|
|
||||||
get() = (dialog as? BottomSheetDialog)?.behavior
|
|
||||||
|
|
||||||
final override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val binding = onInflateView(inflater, container)
|
|
||||||
viewBinding = binding
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return if (resources.getBoolean(R.bool.is_tablet)) {
|
|
||||||
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
|
|
||||||
} else {
|
|
||||||
AppBottomSheetDialog(requireContext(), theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
|
|
||||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
|
||||||
val b = behavior ?: return
|
|
||||||
if (isExpanded) {
|
|
||||||
b.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
}
|
|
||||||
b.isFitToContents = !isExpanded
|
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
|
||||||
rootView?.updateLayoutParams {
|
|
||||||
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
|
|
||||||
}
|
|
||||||
b.isDraggable = !isLocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
|
|
||||||
abstract class BaseFragment<B : ViewBinding> :
|
|
||||||
Fragment(),
|
|
||||||
WindowInsetsDelegate.WindowInsetsListener {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
|
||||||
|
|
||||||
protected val binding: B
|
|
||||||
get() = checkNotNull(viewBinding)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val exceptionResolver = ExceptionResolver(this)
|
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
|
||||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
|
||||||
|
|
||||||
protected val actionModeDelegate: ActionModeDelegate
|
|
||||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val binding = onInflateView(inflater, container)
|
|
||||||
viewBinding = binding
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
insetsDelegate.onViewCreated(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
insetsDelegate.onDestroyView()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun bindingOrNull() = viewBinding
|
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
|
|
||||||
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
|
|
||||||
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
|
|
||||||
abstract class BaseFullscreenActivity<B : ViewBinding> : BaseActivity<B>(),
|
|
||||||
View.OnSystemUiVisibilityChangeListener {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
with(window) {
|
|
||||||
statusBarColor = Color.TRANSPARENT
|
|
||||||
navigationBarColor = Color.TRANSPARENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
|
||||||
}
|
|
||||||
showSystemUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
|
||||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO WindowInsetsControllerCompat works incorrect
|
|
||||||
protected fun hideSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun showSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
|
|
||||||
abstract class BaseService : LifecycleService()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
|
|
||||||
abstract class BaseViewModel : ViewModel() {
|
|
||||||
|
|
||||||
val onError = SingleLiveEvent<Throwable>()
|
|
||||||
val isLoading = CountedBooleanLiveData()
|
|
||||||
|
|
||||||
protected fun launchJob(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
|
||||||
|
|
||||||
protected fun launchLoadingJob(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
|
||||||
block: suspend CoroutineScope.() -> Unit
|
|
||||||
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
|
||||||
isLoading.postValue(true)
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
isLoading.postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
throwable.printStackTrace()
|
|
||||||
}
|
|
||||||
if (throwable !is CancellationException) {
|
|
||||||
onError.postCall(throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
abstract class CoroutineIntentService : BaseService() {
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
protected open val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
super.onStartCommand(intent, flags, startId)
|
|
||||||
launchCoroutine(intent, startId)
|
|
||||||
return Service.START_REDELIVER_INTENT
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
|
|
||||||
mutex.withLock {
|
|
||||||
try {
|
|
||||||
withContext(dispatcher) {
|
|
||||||
processIntent(intent)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
stopSelf(startId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract suspend fun processIntent(intent: Intent?)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemStorageBinding
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
|
|
||||||
DialogInterface by delegate {
|
|
||||||
|
|
||||||
fun show() = delegate.show()
|
|
||||||
|
|
||||||
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
|
|
||||||
|
|
||||||
private val adapter = VolumesAdapter(storageManager)
|
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (adapter.isEmpty) {
|
|
||||||
delegate.setMessage(R.string.cannot_find_available_storage)
|
|
||||||
} else {
|
|
||||||
val defaultValue = runBlocking {
|
|
||||||
storageManager.getDefaultWriteableDir()
|
|
||||||
}
|
|
||||||
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
|
|
||||||
it.first.canonicalPath == defaultValue?.canonicalPath
|
|
||||||
}
|
|
||||||
delegate.setAdapter(adapter) { d, i ->
|
|
||||||
listener.onStorageSelected(adapter.getItem(i).first)
|
|
||||||
d.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
|
||||||
delegate.setTitle(titleResId)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(title: CharSequence): Builder {
|
|
||||||
delegate.setTitle(title)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNegativeButton(@StringRes textId: Int): Builder {
|
|
||||||
delegate.setNegativeButton(textId, null)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create() = StorageSelectDialog(delegate.create())
|
|
||||||
}
|
|
||||||
|
|
||||||
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
|
|
||||||
|
|
||||||
var selectedItemPosition: Int = -1
|
|
||||||
val volumes = getAvailableVolumes(storageManager)
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
|
|
||||||
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
|
|
||||||
view.tag = it
|
|
||||||
}
|
|
||||||
val item = volumes[position]
|
|
||||||
binding.imageViewIndicator.isChecked = selectedItemPosition == position
|
|
||||||
binding.textViewTitle.text = item.second
|
|
||||||
binding.textViewSubtitle.text = item.first.path
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Pair<File, String> = volumes[position]
|
|
||||||
|
|
||||||
override fun getItemId(position: Int) = position.toLong()
|
|
||||||
|
|
||||||
override fun getCount() = volumes.size
|
|
||||||
|
|
||||||
override fun hasStableIds() = true
|
|
||||||
|
|
||||||
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
|
|
||||||
return runBlocking {
|
|
||||||
storageManager.getWriteableDirs().map {
|
|
||||||
it to storageManager.getStorageDisplayName(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnStorageSelectListener {
|
|
||||||
|
|
||||||
fun onStorageSelected(file: File)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.text.InputFilter
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.koitharu.kotatsu.databinding.DialogInputBinding
|
|
||||||
|
|
||||||
class TextInputDialog private constructor(
|
|
||||||
private val delegate: AlertDialog,
|
|
||||||
) : DialogInterface by delegate {
|
|
||||||
|
|
||||||
fun show() = delegate.show()
|
|
||||||
|
|
||||||
class Builder(context: Context) {
|
|
||||||
|
|
||||||
private val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
|
|
||||||
|
|
||||||
private val delegate = MaterialAlertDialogBuilder(context)
|
|
||||||
.setView(binding.root)
|
|
||||||
|
|
||||||
fun setTitle(@StringRes titleResId: Int): Builder {
|
|
||||||
delegate.setTitle(titleResId)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTitle(title: CharSequence): Builder {
|
|
||||||
delegate.setTitle(title)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHint(@StringRes hintResId: Int): Builder {
|
|
||||||
binding.inputEdit.hint = binding.root.context.getString(hintResId)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMaxLength(maxLength: Int, strict: Boolean): Builder {
|
|
||||||
with(binding.inputLayout) {
|
|
||||||
counterMaxLength = maxLength
|
|
||||||
isCounterEnabled = maxLength > 0
|
|
||||||
}
|
|
||||||
if (strict && maxLength > 0) {
|
|
||||||
binding.inputEdit.filters += InputFilter.LengthFilter(maxLength)
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setInputType(inputType: Int): Builder {
|
|
||||||
binding.inputEdit.inputType = inputType
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setText(text: String): Builder {
|
|
||||||
binding.inputEdit.setText(text)
|
|
||||||
binding.inputEdit.setSelection(text.length)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPositiveButton(
|
|
||||||
@StringRes textId: Int,
|
|
||||||
listener: (DialogInterface, String) -> Unit
|
|
||||||
): Builder {
|
|
||||||
delegate.setPositiveButton(textId) { dialog, _ ->
|
|
||||||
listener(dialog, binding.inputEdit.text?.toString().orEmpty())
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNegativeButton(
|
|
||||||
@StringRes textId: Int,
|
|
||||||
listener: DialogInterface.OnClickListener? = null
|
|
||||||
): Builder {
|
|
||||||
delegate.setNegativeButton(textId, listener)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOnCancelListener(listener: DialogInterface.OnCancelListener): Builder {
|
|
||||||
delegate.setOnCancelListener(listener)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create() =
|
|
||||||
TextInputDialog(delegate.create())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
|
|
||||||
class CountedBooleanLiveData : MutableLiveData<Boolean>(false) {
|
|
||||||
|
|
||||||
private var counter = 0
|
|
||||||
|
|
||||||
override fun setValue(value: Boolean) {
|
|
||||||
if (value) {
|
|
||||||
counter++
|
|
||||||
} else {
|
|
||||||
counter--
|
|
||||||
}
|
|
||||||
val newValue = counter > 0
|
|
||||||
if (newValue != this.value) {
|
|
||||||
super.setValue(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.core.view.children
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
|
|
||||||
class CheckableButtonGroup @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0,
|
|
||||||
) : LinearLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
|
||||||
|
|
||||||
var onCheckedChangeListener: OnCheckedChangeListener? = null
|
|
||||||
|
|
||||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
|
||||||
if (child is MaterialButton) {
|
|
||||||
child.setOnClickListener(this)
|
|
||||||
}
|
|
||||||
super.addView(child, index, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
setCheckedId(v.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCheckedId(@IdRes viewRes: Int) {
|
|
||||||
children.forEach {
|
|
||||||
(it as? MaterialButton)?.isChecked = it.id == viewRes
|
|
||||||
}
|
|
||||||
onCheckedChangeListener?.onCheckedChanged(this, viewRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface OnCheckedChangeListener {
|
|
||||||
fun onCheckedChanged(group: CheckableButtonGroup, checkedId: Int)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Google LLC
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
private const val ENTER_DURATION = 300L
|
|
||||||
private const val EXIT_DURATION = 200L
|
|
||||||
private const val SHORT_DURATION = 1_500L
|
|
||||||
private const val LONG_DURATION = 2_750L
|
|
||||||
/**
|
|
||||||
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
|
|
||||||
*
|
|
||||||
* Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
|
|
||||||
*
|
|
||||||
* https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
|
|
||||||
*/
|
|
||||||
class FadingSnackbar @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
private val message: TextView
|
|
||||||
private val action: Button
|
|
||||||
|
|
||||||
init {
|
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.fading_snackbar_layout, this, true)
|
|
||||||
message = view.findViewById(R.id.snackbar_text)
|
|
||||||
action = view.findViewById(R.id.snackbar_action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
if (visibility == VISIBLE && alpha == 1f) {
|
|
||||||
animate()
|
|
||||||
.alpha(0f)
|
|
||||||
.withEndAction { visibility = GONE }
|
|
||||||
.duration = EXIT_DURATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(
|
|
||||||
messageText: CharSequence? = null,
|
|
||||||
@StringRes actionId: Int? = null,
|
|
||||||
longDuration: Boolean = true,
|
|
||||||
actionClick: () -> Unit = { dismiss() },
|
|
||||||
dismissListener: () -> Unit = { }
|
|
||||||
) {
|
|
||||||
message.text = messageText
|
|
||||||
if (actionId != null) {
|
|
||||||
action.run {
|
|
||||||
visibility = VISIBLE
|
|
||||||
text = context.getString(actionId)
|
|
||||||
setOnClickListener {
|
|
||||||
actionClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
action.visibility = GONE
|
|
||||||
}
|
|
||||||
alpha = 0f
|
|
||||||
visibility = VISIBLE
|
|
||||||
animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.duration = ENTER_DURATION
|
|
||||||
val showDuration = ENTER_DURATION + if (longDuration) LONG_DURATION else SHORT_DURATION
|
|
||||||
postDelayed(showDuration) {
|
|
||||||
dismiss()
|
|
||||||
dismissListener()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.widgets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
|
|
||||||
class SquareLayout @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
interface CloudFlareCallback {
|
|
||||||
|
|
||||||
fun onPageLoaded()
|
|
||||||
|
|
||||||
fun onCheckPassed()
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.webkit.WebView
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
|
||||||
import org.koitharu.kotatsu.core.network.WebViewClientCompat
|
|
||||||
|
|
||||||
private const val CF_CLEARANCE = "cf_clearance"
|
|
||||||
|
|
||||||
class CloudFlareClient(
|
|
||||||
private val cookieJar: AndroidCookieJar,
|
|
||||||
private val callback: CloudFlareCallback,
|
|
||||||
private val targetUrl: String
|
|
||||||
) : WebViewClientCompat() {
|
|
||||||
|
|
||||||
private val oldClearance = getCookieValue(CF_CLEARANCE)
|
|
||||||
|
|
||||||
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
|
||||||
super.onPageStarted(view, url, favicon)
|
|
||||||
checkClearance()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
|
||||||
super.onPageCommitVisible(view, url)
|
|
||||||
callback.onPageLoaded()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
super.onPageFinished(view, url)
|
|
||||||
callback.onPageLoaded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkClearance() {
|
|
||||||
val clearance = getCookieValue(CF_CLEARANCE)
|
|
||||||
if (clearance != null && clearance != oldClearance) {
|
|
||||||
callback.onCheckPassed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCookieValue(name: String): String? {
|
|
||||||
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
|
||||||
.find { it.name == name }?.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.fragment.app.setFragmentResult
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
|
||||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
|
||||||
|
|
||||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
|
||||||
|
|
||||||
private val url by stringArgument(ARG_URL)
|
|
||||||
private val pendingResult = Bundle(1)
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?
|
|
||||||
) = FragmentCloudflareBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
with(binding.webView.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
|
||||||
domStorageEnabled = true
|
|
||||||
databaseEnabled = true
|
|
||||||
userAgentString = UserAgentInterceptor.userAgent
|
|
||||||
}
|
|
||||||
binding.webView.webViewClient = CloudFlareClient(get(), this, url.orEmpty())
|
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
} else {
|
|
||||||
binding.webView.loadUrl(url.orEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
binding.webView.stopLoading()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
binding.webView.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
binding.webView.onPause()
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
setFragmentResult(TAG, pendingResult)
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageLoaded() {
|
|
||||||
bindingOrNull()?.progressBar?.isInvisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckPassed() {
|
|
||||||
pendingResult.putBoolean(EXTRA_RESULT, true)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val TAG = "CloudFlareDialog"
|
|
||||||
const val EXTRA_RESULT = "result"
|
|
||||||
private const val ARG_URL = "url"
|
|
||||||
|
|
||||||
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
|
|
||||||
putString(ARG_URL, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
|
||||||
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
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.favourites.data.FavouriteCategoryEntity
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
|
||||||
|
|
||||||
private const val PAGE_SIZE = 10
|
|
||||||
|
|
||||||
class BackupRepository(private val db: MangaDatabase) {
|
|
||||||
|
|
||||||
suspend fun dumpHistory(): BackupEntry {
|
|
||||||
var offset = 0
|
|
||||||
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
|
||||||
while (true) {
|
|
||||||
val history = db.historyDao.findAll(offset, PAGE_SIZE)
|
|
||||||
if (history.isEmpty()) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
offset += history.size
|
|
||||||
for (item in history) {
|
|
||||||
val manga = item.manga.toJson()
|
|
||||||
val tags = JSONArray()
|
|
||||||
item.tags.forEach { tags.put(it.toJson()) }
|
|
||||||
manga.put("tags", tags)
|
|
||||||
val json = item.history.toJson()
|
|
||||||
json.put("manga", manga)
|
|
||||||
entry.data.put(json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun dumpCategories(): BackupEntry {
|
|
||||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
|
||||||
val categories = db.favouriteCategoriesDao.findAll()
|
|
||||||
for (item in categories) {
|
|
||||||
entry.data.put(item.toJson())
|
|
||||||
}
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun dumpFavourites(): BackupEntry {
|
|
||||||
var offset = 0
|
|
||||||
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
|
||||||
while (true) {
|
|
||||||
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
|
|
||||||
if (favourites.isEmpty()) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
offset += favourites.size
|
|
||||||
for (item in favourites) {
|
|
||||||
val manga = item.manga.toJson()
|
|
||||||
val tags = JSONArray()
|
|
||||||
item.tags.forEach { tags.put(it.toJson()) }
|
|
||||||
manga.put("tags", tags)
|
|
||||||
val json = item.favourite.toJson()
|
|
||||||
json.put("manga", manga)
|
|
||||||
entry.data.put(json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createIndex(): BackupEntry {
|
|
||||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("app_id", BuildConfig.APPLICATION_ID)
|
|
||||||
json.put("app_version", BuildConfig.VERSION_CODE)
|
|
||||||
json.put("created_at", System.currentTimeMillis())
|
|
||||||
entry.data.put(json)
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MangaEntity.toJson(): JSONObject {
|
|
||||||
val jo = JSONObject()
|
|
||||||
jo.put("id", id)
|
|
||||||
jo.put("title", title)
|
|
||||||
jo.put("alt_title", altTitle)
|
|
||||||
jo.put("url", url)
|
|
||||||
jo.put("public_url", publicUrl)
|
|
||||||
jo.put("rating", rating)
|
|
||||||
jo.put("nsfw", isNsfw)
|
|
||||||
jo.put("cover_url", coverUrl)
|
|
||||||
jo.put("large_cover_url", largeCoverUrl)
|
|
||||||
jo.put("state", state)
|
|
||||||
jo.put("author", author)
|
|
||||||
jo.put("source", source)
|
|
||||||
return jo
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TagEntity.toJson(): JSONObject {
|
|
||||||
val jo = JSONObject()
|
|
||||||
jo.put("id", id)
|
|
||||||
jo.put("title", title)
|
|
||||||
jo.put("key", key)
|
|
||||||
jo.put("source", source)
|
|
||||||
return jo
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun HistoryEntity.toJson(): JSONObject {
|
|
||||||
val jo = JSONObject()
|
|
||||||
jo.put("manga_id", mangaId)
|
|
||||||
jo.put("created_at", createdAt)
|
|
||||||
jo.put("updated_at", updatedAt)
|
|
||||||
jo.put("chapter_id", chapterId)
|
|
||||||
jo.put("page", page)
|
|
||||||
jo.put("scroll", scroll)
|
|
||||||
return jo
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun FavouriteCategoryEntity.toJson(): JSONObject {
|
|
||||||
val jo = JSONObject()
|
|
||||||
jo.put("category_id", categoryId)
|
|
||||||
jo.put("created_at", createdAt)
|
|
||||||
jo.put("sort_key", sortKey)
|
|
||||||
jo.put("title", title)
|
|
||||||
jo.put("order", order)
|
|
||||||
return jo
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun FavouriteEntity.toJson(): JSONObject {
|
|
||||||
val jo = JSONObject()
|
|
||||||
jo.put("manga_id", mangaId)
|
|
||||||
jo.put("category_id", categoryId)
|
|
||||||
jo.put("created_at", createdAt)
|
|
||||||
return jo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
|
||||||
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.favourites.data.FavouriteCategoryEntity
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
||||||
|
|
||||||
class RestoreRepository(private val db: MangaDatabase) {
|
|
||||||
|
|
||||||
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
|
|
||||||
val result = CompositeResult()
|
|
||||||
for (item in entry.data.JSONIterator()) {
|
|
||||||
val mangaJson = item.getJSONObject("manga")
|
|
||||||
val manga = parseManga(mangaJson)
|
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
|
||||||
parseTag(it)
|
|
||||||
}
|
|
||||||
val history = parseHistory(item)
|
|
||||||
result += runCatching {
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga, tags)
|
|
||||||
db.historyDao.upsert(history)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
|
|
||||||
val result = CompositeResult()
|
|
||||||
for (item in entry.data.JSONIterator()) {
|
|
||||||
val category = parseCategory(item)
|
|
||||||
result += runCatching {
|
|
||||||
db.favouriteCategoriesDao.upsert(category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
|
|
||||||
val result = CompositeResult()
|
|
||||||
for (item in entry.data.JSONIterator()) {
|
|
||||||
val mangaJson = item.getJSONObject("manga")
|
|
||||||
val manga = parseManga(mangaJson)
|
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
|
||||||
parseTag(it)
|
|
||||||
}
|
|
||||||
val favourite = parseFavourite(item)
|
|
||||||
result += runCatching {
|
|
||||||
db.withTransaction {
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(manga, tags)
|
|
||||||
db.favouritesDao.upsert(favourite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseManga(json: JSONObject) = MangaEntity(
|
|
||||||
id = json.getLong("id"),
|
|
||||||
title = json.getString("title"),
|
|
||||||
altTitle = json.getStringOrNull("alt_title"),
|
|
||||||
url = json.getString("url"),
|
|
||||||
publicUrl = json.getStringOrNull("public_url").orEmpty(),
|
|
||||||
rating = json.getDouble("rating").toFloat(),
|
|
||||||
isNsfw = json.getBooleanOrDefault("nsfw", false),
|
|
||||||
coverUrl = json.getString("cover_url"),
|
|
||||||
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
|
||||||
state = json.getStringOrNull("state"),
|
|
||||||
author = json.getStringOrNull("author"),
|
|
||||||
source = json.getString("source")
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseTag(json: JSONObject) = TagEntity(
|
|
||||||
id = json.getLong("id"),
|
|
||||||
title = json.getString("title"),
|
|
||||||
key = json.getString("key"),
|
|
||||||
source = json.getString("source")
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseHistory(json: JSONObject) = HistoryEntity(
|
|
||||||
mangaId = json.getLong("manga_id"),
|
|
||||||
createdAt = json.getLong("created_at"),
|
|
||||||
updatedAt = json.getLong("updated_at"),
|
|
||||||
chapterId = json.getLong("chapter_id"),
|
|
||||||
page = json.getInt("page"),
|
|
||||||
scroll = json.getDouble("scroll").toFloat()
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
|
|
||||||
categoryId = json.getInt("category_id"),
|
|
||||||
createdAt = json.getLong("created_at"),
|
|
||||||
sortKey = json.getInt("sort_key"),
|
|
||||||
title = json.getString("title"),
|
|
||||||
order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
|
|
||||||
mangaId = json.getLong("manga_id"),
|
|
||||||
categoryId = json.getLong("category_id"),
|
|
||||||
createdAt = json.getLong("created_at")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val databaseModule
|
|
||||||
get() = module {
|
|
||||||
single { MangaDatabase.create(androidContext()) }
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import org.koitharu.kotatsu.core.db.dao.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.*
|
|
||||||
import org.koitharu.kotatsu.core.db.migrations.*
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoriesDao
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouritesDao
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryDao
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
|
||||||
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
|
|
||||||
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
|
||||||
|
|
||||||
@Database(
|
|
||||||
entities = [
|
|
||||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
|
||||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
|
||||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class
|
|
||||||
],
|
|
||||||
version = 9
|
|
||||||
)
|
|
||||||
abstract class MangaDatabase : RoomDatabase() {
|
|
||||||
|
|
||||||
abstract val historyDao: HistoryDao
|
|
||||||
|
|
||||||
abstract val tagsDao: TagsDao
|
|
||||||
|
|
||||||
abstract val mangaDao: MangaDao
|
|
||||||
|
|
||||||
abstract val favouritesDao: FavouritesDao
|
|
||||||
|
|
||||||
abstract val preferencesDao: PreferencesDao
|
|
||||||
|
|
||||||
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
|
||||||
|
|
||||||
abstract val tracksDao: TracksDao
|
|
||||||
|
|
||||||
abstract val trackLogsDao: TrackLogsDao
|
|
||||||
|
|
||||||
abstract val suggestionDao: SuggestionDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(context: Context): MangaDatabase = Room.databaseBuilder(
|
|
||||||
context,
|
|
||||||
MangaDatabase::class.java,
|
|
||||||
"kotatsu-db"
|
|
||||||
).addMigrations(
|
|
||||||
Migration1To2(),
|
|
||||||
Migration2To3(),
|
|
||||||
Migration3To4(),
|
|
||||||
Migration4To5(),
|
|
||||||
Migration5To6(),
|
|
||||||
Migration6To7(),
|
|
||||||
Migration7To8(),
|
|
||||||
Migration8To9(),
|
|
||||||
).addCallback(
|
|
||||||
DatabasePrePopulateCallback(context.resources)
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class PreferencesDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun find(mangaId: Long): MangaPrefsEntity?
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun insert(pref: MangaPrefsEntity): Long
|
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(pref: MangaPrefsEntity): Int
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(pref: MangaPrefsEntity) {
|
|
||||||
if (update(pref) == 0) {
|
|
||||||
insert(pref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
|
||||||
|
|
||||||
import androidx.room.*
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.TrackEntity
|
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class TracksDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tracks")
|
|
||||||
abstract suspend fun findAll(): List<TrackEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun find(mangaId: Long): TrackEntity?
|
|
||||||
|
|
||||||
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun findNewChapters(mangaId: Long): Int?
|
|
||||||
|
|
||||||
@Query("DELETE FROM tracks")
|
|
||||||
abstract suspend fun clear()
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
abstract suspend fun insert(entity: TrackEntity): Long
|
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(entity: TrackEntity): Int
|
|
||||||
|
|
||||||
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
|
|
||||||
abstract suspend fun delete(mangaId: Long)
|
|
||||||
|
|
||||||
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
|
|
||||||
abstract suspend fun cleanup()
|
|
||||||
|
|
||||||
@Transaction
|
|
||||||
open suspend fun upsert(entity: TrackEntity) {
|
|
||||||
if (update(entity) == 0) {
|
|
||||||
insert(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.db.entity
|
|
||||||
|
|
||||||
import androidx.room.Embedded
|
|
||||||
import androidx.room.Junction
|
|
||||||
import androidx.room.Relation
|
|
||||||
|
|
||||||
class MangaWithTags(
|
|
||||||
@Embedded val manga: MangaEntity,
|
|
||||||
@Relation(
|
|
||||||
parentColumn = "manga_id",
|
|
||||||
entityColumn = "tag_id",
|
|
||||||
associateBy = Junction(MangaTagsEntity::class)
|
|
||||||
)
|
|
||||||
val tags: List<TagEntity>,
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import okio.IOException
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
|
||||||
val url: String
|
|
||||||
) : IOException("Protected by CloudFlare")
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
class MangaNotFoundException(s: String? = null) : RuntimeException(s)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
class WrongPasswordException : SecurityException()
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions.resolve
|
|
||||||
|
|
||||||
import android.util.ArrayMap
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
|
||||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
|
||||||
import org.koitharu.kotatsu.utils.isSuccess
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
class ExceptionResolver private constructor(
|
|
||||||
private val activity: FragmentActivity?,
|
|
||||||
private val fragment: Fragment?,
|
|
||||||
) : ActivityResultCallback<TaggedActivityResult> {
|
|
||||||
|
|
||||||
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
|
|
||||||
private lateinit var sourceAuthContract: ActivityResultLauncher<MangaSource>
|
|
||||||
|
|
||||||
constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) {
|
|
||||||
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
|
|
||||||
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(result: TaggedActivityResult?) {
|
|
||||||
result ?: return
|
|
||||||
continuations.remove(result.tag)?.resume(result.isSuccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
|
||||||
is CloudFlareProtectedException -> resolveCF(e.url)
|
|
||||||
is AuthRequiredException -> resolveAuthException(e.source)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun resolveCF(url: String): Boolean {
|
|
||||||
val dialog = CloudFlareDialog.newInstance(url)
|
|
||||||
val fm = getFragmentManager()
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
fm.clearFragmentResult(CloudFlareDialog.TAG)
|
|
||||||
continuations[CloudFlareDialog.TAG] = cont
|
|
||||||
fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
|
|
||||||
continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
|
|
||||||
}
|
|
||||||
dialog.show(fm, CloudFlareDialog.TAG)
|
|
||||||
cont.invokeOnCancellation {
|
|
||||||
continuations.remove(CloudFlareDialog.TAG, cont)
|
|
||||||
fm.clearFragmentResultListener(CloudFlareDialog.TAG)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
|
||||||
continuations[SourceAuthActivity.TAG] = cont
|
|
||||||
sourceAuthContract.launch(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@StringRes
|
|
||||||
fun getResolveStringId(e: Throwable) = when (e) {
|
|
||||||
is CloudFlareProtectedException -> R.string.captcha_solve
|
|
||||||
is AuthRequiredException -> R.string.sign_in
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.github
|
|
||||||
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val githubModule
|
|
||||||
get() = module {
|
|
||||||
single {
|
|
||||||
GithubRepository(get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.github
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
|
||||||
|
|
||||||
class GithubRepository(private val okHttp: OkHttpClient) {
|
|
||||||
|
|
||||||
suspend fun getLatestVersion(): AppVersion {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.get()
|
|
||||||
.url("https://api.github.com/repos/nv95/Kotatsu/releases/latest")
|
|
||||||
val json = okHttp.newCall(request.build()).await().parseJson()
|
|
||||||
val asset = json.getJSONArray("assets").getJSONObject(0)
|
|
||||||
return AppVersion(
|
|
||||||
id = json.getLong("id"),
|
|
||||||
url = json.getString("html_url"),
|
|
||||||
name = json.getString("name").removePrefix("v"),
|
|
||||||
apkSize = asset.getLong("size"),
|
|
||||||
apkUrl = asset.getString("browser_download_url"),
|
|
||||||
description = json.getString("body")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
|
|
||||||
fun Collection<Manga>.ids() = mapToSet { it.id }
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class MangaTracking(
|
|
||||||
val manga: Manga,
|
|
||||||
val knownChaptersCount: Int,
|
|
||||||
val lastChapterId: Long,
|
|
||||||
val lastNotifiedChapterId: Long,
|
|
||||||
val lastCheck: Date?
|
|
||||||
)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.koin.dsl.bind
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
|
|
||||||
val networkModule
|
|
||||||
get() = module {
|
|
||||||
single { AndroidCookieJar() } bind CookieJar::class
|
|
||||||
single {
|
|
||||||
OkHttpClient.Builder().apply {
|
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
|
||||||
cookieJar(get())
|
|
||||||
cache(get<LocalStorageManager>().createHttpCache())
|
|
||||||
addInterceptor(UserAgentInterceptor())
|
|
||||||
addInterceptor(CloudFlareInterceptor())
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
single<MangaLoaderContext> { MangaLoaderContextImpl(get(), get(), get()) }
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import java.util.*
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
|
|
||||||
class UserAgentInterceptor : Interceptor {
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
return chain.proceed(
|
|
||||||
if (request.header(CommonHeaders.USER_AGENT) == null) {
|
|
||||||
request.newBuilder()
|
|
||||||
.addHeader(CommonHeaders.USER_AGENT, userAgent)
|
|
||||||
.build()
|
|
||||||
} else request
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val userAgent
|
|
||||||
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
|
|
||||||
BuildConfig.VERSION_NAME,
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
Build.MODEL,
|
|
||||||
Build.BRAND,
|
|
||||||
Build.DEVICE,
|
|
||||||
Locale.getDefault().language
|
|
||||||
)
|
|
||||||
|
|
||||||
val userAgentChrome
|
|
||||||
get() = (
|
|
||||||
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) " +
|
|
||||||
"Chrome/100.0.4896.127 Mobile Safari/537.36"
|
|
||||||
).format(
|
|
||||||
Build.VERSION.RELEASE,
|
|
||||||
Build.MODEL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.*
|
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
|
||||||
abstract class WebViewClientCompat : WebViewClient() {
|
|
||||||
|
|
||||||
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onReceivedErrorCompat(
|
|
||||||
view: WebView,
|
|
||||||
errorCode: Int,
|
|
||||||
description: String?,
|
|
||||||
failingUrl: String,
|
|
||||||
isMainFrame: Boolean
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
|
||||||
final override fun shouldOverrideUrlLoading(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): Boolean = shouldOverrideUrlCompat(view, request.url.toString())
|
|
||||||
|
|
||||||
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
|
||||||
return shouldOverrideUrlCompat(view, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun shouldInterceptRequest(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): WebResourceResponse? = shouldInterceptRequestCompat(view, request.url.toString())
|
|
||||||
|
|
||||||
final override fun shouldInterceptRequest(
|
|
||||||
view: WebView,
|
|
||||||
url: String
|
|
||||||
): WebResourceResponse? = shouldInterceptRequestCompat(view, url)
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
final override fun onReceivedError(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
error: WebResourceError
|
|
||||||
) {
|
|
||||||
onReceivedErrorCompat(
|
|
||||||
view,
|
|
||||||
error.errorCode,
|
|
||||||
error.description?.toString(),
|
|
||||||
request.url.toString(),
|
|
||||||
request.isForMainFrame
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun onReceivedError(
|
|
||||||
view: WebView,
|
|
||||||
errorCode: Int,
|
|
||||||
description: String?,
|
|
||||||
failingUrl: String
|
|
||||||
) {
|
|
||||||
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
final override fun onReceivedHttpError(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
error: WebResourceResponse
|
|
||||||
) {
|
|
||||||
onReceivedErrorCompat(
|
|
||||||
view,
|
|
||||||
error.statusCode,
|
|
||||||
error.reasonPhrase,
|
|
||||||
request.url
|
|
||||||
.toString(),
|
|
||||||
request.isForMainFrame
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.os
|
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ShortcutManager
|
|
||||||
import android.media.ThumbnailUtils
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.PixelSize
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|
||||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
|
||||||
|
|
||||||
class ShortcutsRepository(
|
|
||||||
private val context: Context,
|
|
||||||
private val coil: ImageLoader,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val mangaRepository: MangaDataRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val iconSize by lazy {
|
|
||||||
getIconSize(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateShortcuts() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return
|
|
||||||
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
|
|
||||||
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
|
|
||||||
.filter { x -> x.title.isNotEmpty() }
|
|
||||||
.map { buildShortcutInfo(it).build().toShortcutInfo() }
|
|
||||||
manager.dynamicShortcuts = shortcuts
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun requestPinShortcut(manga: Manga): Boolean {
|
|
||||||
return ShortcutManagerCompat.requestPinShortcut(
|
|
||||||
context,
|
|
||||||
buildShortcutInfo(manga).build(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
|
|
||||||
val icon = runCatching {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val bmp = coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(manga.coverUrl)
|
|
||||||
.size(iconSize)
|
|
||||||
.build()
|
|
||||||
).requireBitmap()
|
|
||||||
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
|
|
||||||
}
|
|
||||||
}.fold(
|
|
||||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
|
||||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }
|
|
||||||
)
|
|
||||||
mangaRepository.storeManga(manga)
|
|
||||||
return ShortcutInfoCompat.Builder(context, manga.id.toString())
|
|
||||||
.setShortLabel(manga.title)
|
|
||||||
.setLongLabel(manga.title)
|
|
||||||
.setIcon(icon)
|
|
||||||
.setIntent(
|
|
||||||
ReaderActivity.newIntent(context, manga.id)
|
|
||||||
.setAction(ReaderActivity.ACTION_MANGA_READ)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getIconSize(context: Context): PixelSize {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
|
||||||
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
|
|
||||||
PixelSize(it.iconMaxWidth, it.iconMaxHeight)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
|
|
||||||
PixelSize(it, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import coil.map.Mapper
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
class FaviconMapper() : Mapper<Uri, HttpUrl> {
|
|
||||||
|
|
||||||
override fun map(data: Uri): HttpUrl {
|
|
||||||
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
|
|
||||||
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
|
|
||||||
return repo.getFaviconUrl().toHttpUrl()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(data: Uri) = data.scheme == "favicon"
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.*
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.get
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
|
|
||||||
interface MangaRepository {
|
|
||||||
|
|
||||||
val source: MangaSource
|
|
||||||
|
|
||||||
val sortOrders: Set<SortOrder>
|
|
||||||
|
|
||||||
suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String? = null,
|
|
||||||
tags: Set<MangaTag>? = null,
|
|
||||||
sortOrder: SortOrder? = null,
|
|
||||||
): List<Manga>
|
|
||||||
|
|
||||||
suspend fun getDetails(manga: Manga): Manga
|
|
||||||
|
|
||||||
suspend fun getPages(chapter: MangaChapter): List<MangaPage>
|
|
||||||
|
|
||||||
suspend fun getPageUrl(page: MangaPage): String
|
|
||||||
|
|
||||||
suspend fun getTags(): Set<MangaTag>
|
|
||||||
|
|
||||||
companion object : KoinComponent {
|
|
||||||
|
|
||||||
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
|
||||||
|
|
||||||
operator fun invoke(source: MangaSource): MangaRepository {
|
|
||||||
if (source == MangaSource.LOCAL) {
|
|
||||||
return get<LocalMangaRepository>()
|
|
||||||
}
|
|
||||||
cache[source]?.get()?.let { return it }
|
|
||||||
return synchronized(cache) {
|
|
||||||
cache[source]?.get()?.let { return it }
|
|
||||||
val repository = RemoteMangaRepository(MangaParser(source, get()))
|
|
||||||
cache[source] = WeakReference(repository)
|
|
||||||
repository
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
|
|
||||||
class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
|
||||||
|
|
||||||
override val source: MangaSource
|
|
||||||
get() = parser.source
|
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
|
||||||
get() = parser.sortOrders
|
|
||||||
|
|
||||||
var defaultSortOrder: SortOrder?
|
|
||||||
get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull()
|
|
||||||
set(value) {
|
|
||||||
getConfig().defaultSortOrder = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getList(
|
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder?,
|
|
||||||
): List<Manga> = parser.getList(offset, query, tags, sortOrder)
|
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga)
|
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = parser.getPages(chapter)
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> = parser.getTags()
|
|
||||||
|
|
||||||
fun getFaviconUrl(): String = parser.getFaviconUrl()
|
|
||||||
|
|
||||||
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
|
|
||||||
|
|
||||||
fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also {
|
|
||||||
parser.onCreateConfig(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getConfig() = parser.config as SourceSettings
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
enum class AppSection {
|
|
||||||
|
|
||||||
LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.collection.arraySetOf
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.color.DynamicColors
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AppSettings(context: Context) {
|
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
|
||||||
remove(MangaSource.LOCAL)
|
|
||||||
if (!BuildConfig.DEBUG) {
|
|
||||||
remove(MangaSource.DUMMY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val remoteMangaSources: Set<MangaSource>
|
|
||||||
get() = Collections.unmodifiableSet(remoteSources)
|
|
||||||
|
|
||||||
var listMode: ListMode
|
|
||||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.DETAILED_LIST)
|
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
|
||||||
|
|
||||||
var defaultSection: AppSection
|
|
||||||
get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY)
|
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) }
|
|
||||||
|
|
||||||
val theme: Int
|
|
||||||
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
|
|
||||||
val isDynamicTheme: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_DYNAMIC_THEME, false)
|
|
||||||
|
|
||||||
val isAmoledTheme: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
|
|
||||||
|
|
||||||
var gridSize: Int
|
|
||||||
get() = prefs.getInt(KEY_GRID_SIZE, 100)
|
|
||||||
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
|
|
||||||
|
|
||||||
val readerPageSwitch: Set<String>
|
|
||||||
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
|
|
||||||
|
|
||||||
var isTrafficWarningEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
|
|
||||||
|
|
||||||
var isAllFavouritesVisible: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
|
|
||||||
|
|
||||||
val isUpdateCheckingEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true)
|
|
||||||
|
|
||||||
var lastUpdateCheckTimestamp: Long
|
|
||||||
get() = prefs.getLong(KEY_APP_UPDATE, 0L)
|
|
||||||
set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) }
|
|
||||||
|
|
||||||
val trackerNotifications: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
|
|
||||||
|
|
||||||
var notificationSound: Uri
|
|
||||||
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
|
|
||||||
?: Settings.System.DEFAULT_NOTIFICATION_URI
|
|
||||||
set(value) = prefs.edit { putString(KEY_NOTIFICATIONS_SOUND, value.toString()) }
|
|
||||||
|
|
||||||
val notificationVibrate: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_NOTIFICATIONS_VIBRATE, false)
|
|
||||||
|
|
||||||
val notificationLight: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true)
|
|
||||||
|
|
||||||
val readerAnimation: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
|
|
||||||
|
|
||||||
val isPreferRtlReader: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_READER_PREFER_RTL, false)
|
|
||||||
|
|
||||||
var historyGrouping: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
|
|
||||||
|
|
||||||
val isHistoryExcludeNsfw: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
|
|
||||||
|
|
||||||
var chaptersReverse: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_REVERSE_CHAPTERS, false)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_REVERSE_CHAPTERS, value) }
|
|
||||||
|
|
||||||
val zoomMode: ZoomMode
|
|
||||||
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
|
|
||||||
|
|
||||||
val trackSources: Set<String>
|
|
||||||
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY)
|
|
||||||
|
|
||||||
var appPassword: String?
|
|
||||||
get() = prefs.getString(KEY_APP_PASSWORD, null)
|
|
||||||
set(value) = prefs.edit { if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD) }
|
|
||||||
|
|
||||||
var sourcesOrder: List<String>
|
|
||||||
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
|
||||||
?.split('|')
|
|
||||||
.orEmpty()
|
|
||||||
set(value) = prefs.edit {
|
|
||||||
putString(KEY_SOURCES_ORDER, value.joinToString("|"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var hiddenSources: Set<String>
|
|
||||||
get() = prefs.getStringSet(KEY_SOURCES_HIDDEN, null) ?: emptySet()
|
|
||||||
set(value) = prefs.edit { putStringSet(KEY_SOURCES_HIDDEN, value) }
|
|
||||||
|
|
||||||
val isSourcesSelected: Boolean
|
|
||||||
get() = KEY_SOURCES_HIDDEN in prefs
|
|
||||||
|
|
||||||
val newSources: Set<MangaSource>
|
|
||||||
get() {
|
|
||||||
val known = sourcesOrder.toSet()
|
|
||||||
val hidden = hiddenSources
|
|
||||||
return remoteMangaSources
|
|
||||||
.filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
|
|
||||||
x.name in known || x.name in hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markKnownSources(sources: Collection<MangaSource>) {
|
|
||||||
sourcesOrder = sourcesOrder + sources.map { it.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
val isPagesNumbersEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
|
||||||
|
|
||||||
val screenshotsPolicy: ScreenshotsPolicy
|
|
||||||
get() = runCatching {
|
|
||||||
val key = prefs.getString(KEY_SCREENSHOTS_POLICY, null)?.uppercase(Locale.ROOT)
|
|
||||||
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
|
|
||||||
}.getOrDefault(ScreenshotsPolicy.ALLOW)
|
|
||||||
|
|
||||||
var mangaStorageDir: File?
|
|
||||||
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
|
|
||||||
File(it)
|
|
||||||
}?.takeIf { it.exists() }
|
|
||||||
set(value) = prefs.edit {
|
|
||||||
if (value == null) {
|
|
||||||
remove(KEY_LOCAL_STORAGE)
|
|
||||||
} else {
|
|
||||||
putString(KEY_LOCAL_STORAGE, value.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDownloadsSlowdownEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false)
|
|
||||||
|
|
||||||
val downloadsParallelism: Int
|
|
||||||
get() = prefs.getInt(KEY_DOWNLOADS_PARALLELISM, 2)
|
|
||||||
|
|
||||||
val isSuggestionsEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
|
|
||||||
|
|
||||||
val isSuggestionsExcludeNsfw: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
|
|
||||||
|
|
||||||
var isSearchSingleSource: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_SEARCH_SINGLE_SOURCE, false)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SEARCH_SINGLE_SOURCE, value) }
|
|
||||||
|
|
||||||
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
|
||||||
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
|
||||||
NETWORK_ALWAYS -> true
|
|
||||||
NETWORK_NEVER -> false
|
|
||||||
else -> cm.isActiveNetworkMetered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
|
||||||
when (format) {
|
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSuggestionsTagsBlacklistRegex(): Regex? {
|
|
||||||
val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',')
|
|
||||||
if (string.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val tags = string.split(',')
|
|
||||||
val regex = tags.joinToString(prefix = "(", separator = "|", postfix = ")") { tag ->
|
|
||||||
Regex.escape(tag.trim())
|
|
||||||
}
|
|
||||||
return Regex(regex, RegexOption.IGNORE_CASE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
|
|
||||||
val list = remoteSources.toMutableList()
|
|
||||||
val order = sourcesOrder
|
|
||||||
list.sortBy { x ->
|
|
||||||
val e = order.indexOf(x.name)
|
|
||||||
if (e == -1) order.size + x.ordinal else e
|
|
||||||
}
|
|
||||||
if (!includeHidden) {
|
|
||||||
val hidden = hiddenSources
|
|
||||||
list.removeAll { x -> x.name in hidden }
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
|
||||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unsubscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observe() = callbackFlow<String> {
|
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
|
||||||
trySendBlocking(key)
|
|
||||||
}
|
|
||||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
|
||||||
awaitClose {
|
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val PAGE_SWITCH_TAPS = "taps"
|
|
||||||
const val PAGE_SWITCH_VOLUME_KEYS = "volume"
|
|
||||||
|
|
||||||
const val TRACK_HISTORY = "history"
|
|
||||||
const val TRACK_FAVOURITES = "favourites"
|
|
||||||
|
|
||||||
const val KEY_LIST_MODE = "list_mode_2"
|
|
||||||
const val KEY_APP_SECTION = "app_section_2"
|
|
||||||
const val KEY_THEME = "theme"
|
|
||||||
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
|
||||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
|
||||||
const val KEY_DATE_FORMAT = "date_format"
|
|
||||||
const val KEY_SOURCES_ORDER = "sources_order_2"
|
|
||||||
const val KEY_SOURCES_HIDDEN = "sources_hidden"
|
|
||||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
|
||||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
|
||||||
const val KEY_COOKIES_CLEAR = "cookies_clear"
|
|
||||||
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
|
|
||||||
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
|
|
||||||
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"
|
|
||||||
const val KEY_GRID_SIZE = "grid_size"
|
|
||||||
const val KEY_REMOTE_SOURCES = "remote_sources"
|
|
||||||
const val KEY_LOCAL_STORAGE = "local_storage"
|
|
||||||
const val KEY_READER_SWITCHERS = "reader_switchers"
|
|
||||||
const val KEY_TRACK_SOURCES = "track_sources"
|
|
||||||
const val KEY_TRACK_WARNING = "track_warning"
|
|
||||||
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
|
|
||||||
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
|
|
||||||
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
|
|
||||||
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
|
|
||||||
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
|
|
||||||
const val KEY_READER_ANIMATION = "reader_animation"
|
|
||||||
const val KEY_READER_PREFER_RTL = "reader_prefer_rtl"
|
|
||||||
const val KEY_APP_PASSWORD = "app_password"
|
|
||||||
const val KEY_PROTECT_APP = "protect_app"
|
|
||||||
const val KEY_APP_VERSION = "app_version"
|
|
||||||
const val KEY_ZOOM_MODE = "zoom_mode"
|
|
||||||
const val KEY_BACKUP = "backup"
|
|
||||||
const val KEY_RESTORE = "restore"
|
|
||||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
|
||||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
|
||||||
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
|
|
||||||
const val KEY_PAGES_NUMBERS = "pages_numbers"
|
|
||||||
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
|
|
||||||
const val KEY_PAGES_PRELOAD = "pages_preload"
|
|
||||||
const val KEY_SUGGESTIONS = "suggestions"
|
|
||||||
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
|
|
||||||
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
|
|
||||||
const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source"
|
|
||||||
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
|
|
||||||
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
|
|
||||||
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
|
|
||||||
|
|
||||||
// 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_DISCORD = "about_feedback_discord"
|
|
||||||
const val KEY_FEEDBACK_GITHUB = "about_feedback_github"
|
|
||||||
|
|
||||||
private const val NETWORK_NEVER = 0
|
|
||||||
private const val NETWORK_ALWAYS = 1
|
|
||||||
private const val NETWORK_NON_METERED = 2
|
|
||||||
|
|
||||||
val isDynamicColorAvailable: Boolean
|
|
||||||
get() = DynamicColors.isDynamicColorAvailable() ||
|
|
||||||
(isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
||||||
|
|
||||||
private val isSamsung
|
|
||||||
get() = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler {
|
|
||||||
|
|
||||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
|
||||||
val intent = CrashActivity.newIntent(applicationContext, e)
|
|
||||||
intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
try {
|
|
||||||
applicationContext.startActivity(intent)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
t.printStackTrace()
|
|
||||||
}
|
|
||||||
Log.e("CRASH", e.message, e)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivityCrashBinding
|
|
||||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
|
||||||
import org.koitharu.kotatsu.parsers.util.ellipsize
|
|
||||||
import org.koitharu.kotatsu.utils.ShareHelper
|
|
||||||
|
|
||||||
class CrashActivity : Activity(), View.OnClickListener {
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityCrashBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
binding.textView.text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
|
||||||
binding.buttonClose.setOnClickListener(this)
|
|
||||||
binding.buttonRestart.setOnClickListener(this)
|
|
||||||
binding.buttonReport.setOnClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.opt_crash, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_share -> {
|
|
||||||
ShareHelper(this).shareText(binding.textView.text.toString())
|
|
||||||
}
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_close -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
R.id.button_restart -> {
|
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
|
||||||
intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
R.id.button_report -> {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse("https://github.com/nv95/Kotatsu/issues")
|
|
||||||
try {
|
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.report_github)))
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val MAX_TRACE_SIZE = 131071
|
|
||||||
|
|
||||||
fun newIntent(context: Context, error: Throwable): Intent {
|
|
||||||
val crashInfo = error
|
|
||||||
.stackTraceToString()
|
|
||||||
.trimIndent()
|
|
||||||
.ellipsize(MAX_TRACE_SIZE)
|
|
||||||
val intent = Intent(context, CrashActivity::class.java)
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, crashInfo)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
|
||||||
|
|
||||||
import coil.ComponentRegistry
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.util.CoilUtils
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import org.koitharu.kotatsu.core.parser.FaviconMapper
|
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
|
||||||
|
|
||||||
val uiModule
|
|
||||||
get() = module {
|
|
||||||
single {
|
|
||||||
val httpClientFactory = {
|
|
||||||
get<OkHttpClient>().newBuilder()
|
|
||||||
.cache(CoilUtils.createDefaultCache(androidContext()))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
ImageLoader.Builder(androidContext())
|
|
||||||
.okHttpClient(httpClientFactory)
|
|
||||||
.launchInterceptorChainOnMainThread(false)
|
|
||||||
.componentRegistry(
|
|
||||||
ComponentRegistry.Builder()
|
|
||||||
.add(CbzFetcher())
|
|
||||||
.add(FaviconMapper())
|
|
||||||
.build()
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details
|
|
||||||
|
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
|
||||||
|
|
||||||
val detailsModule
|
|
||||||
get() = module {
|
|
||||||
|
|
||||||
viewModel { intent ->
|
|
||||||
DetailsViewModel(intent.get(), get(), get(), get(), get(), get(), get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
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.databinding.FragmentChaptersBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
|
||||||
import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class ChaptersFragment :
|
|
||||||
BaseFragment<FragmentChaptersBinding>(),
|
|
||||||
OnListItemClickListener<ChapterListItem>,
|
|
||||||
ActionMode.Callback,
|
|
||||||
AdapterView.OnItemSelectedListener,
|
|
||||||
MenuItem.OnActionExpandListener,
|
|
||||||
SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
|
||||||
|
|
||||||
private var chaptersAdapter: ChaptersAdapter? = null
|
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
private var selectionDecoration: ChaptersSelectionDecoration? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?
|
|
||||||
) = FragmentChaptersBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
chaptersAdapter = ChaptersAdapter(this)
|
|
||||||
selectionDecoration = ChaptersSelectionDecoration(view.context)
|
|
||||||
with(binding.recyclerViewChapters) {
|
|
||||||
addItemDecoration(selectionDecoration!!)
|
|
||||||
setHasFixedSize(true)
|
|
||||||
adapter = chaptersAdapter
|
|
||||||
}
|
|
||||||
binding.spinnerBranches?.let(::initSpinner)
|
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
|
||||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
|
||||||
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
|
|
||||||
binding.textViewHolder.isVisible = it
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
chaptersAdapter = null
|
|
||||||
selectionDecoration = null
|
|
||||||
binding.spinnerBranches?.adapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
inflater.inflate(R.menu.opt_chapters, menu)
|
|
||||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
|
||||||
searchMenuItem.setOnActionExpandListener(this)
|
|
||||||
val searchView = searchMenuItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this)
|
|
||||||
searchView.setIconifiedByDefault(false)
|
|
||||||
searchView.queryHint = searchMenuItem.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
||||||
super.onPrepareOptionsMenu(menu)
|
|
||||||
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
|
|
||||||
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.action_reversed -> {
|
|
||||||
viewModel.setChaptersReversed(!item.isChecked)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
|
||||||
if (selectionDecoration?.checkedItemsCount != 0) {
|
|
||||||
selectionDecoration?.toggleItemChecked(item.chapter.id)
|
|
||||||
if (selectionDecoration?.checkedItemsCount == 0) {
|
|
||||||
actionMode?.finish()
|
|
||||||
} else {
|
|
||||||
actionMode?.invalidate()
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
|
|
||||||
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val options = ActivityOptions.makeScaleUpAnimation(
|
|
||||||
view,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
view.measuredWidth,
|
|
||||||
view.measuredHeight
|
|
||||||
)
|
|
||||||
startActivity(
|
|
||||||
ReaderActivity.newIntent(
|
|
||||||
context = view.context,
|
|
||||||
manga = viewModel.manga.value ?: return,
|
|
||||||
state = ReaderState(item.chapter.id, 0, 0),
|
|
||||||
),
|
|
||||||
options.toBundle()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: ChapterListItem, view: View): Boolean {
|
|
||||||
if (actionMode == null) {
|
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
return actionMode?.also {
|
|
||||||
selectionDecoration?.setItemIsChecked(item.chapter.id, true)
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
it.invalidate()
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.action_save -> {
|
|
||||||
DownloadService.start(
|
|
||||||
context ?: return false,
|
|
||||||
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
|
|
||||||
selectionDecoration?.checkedItemsIds?.toSet()
|
|
||||||
)
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_delete -> {
|
|
||||||
val ids = selectionDecoration?.checkedItemsIds
|
|
||||||
val manga = viewModel.manga.value
|
|
||||||
when {
|
|
||||||
ids.isNullOrEmpty() || manga == null -> Unit
|
|
||||||
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
|
|
||||||
else -> {
|
|
||||||
LocalChaptersRemoveService.start(requireContext(), manga, ids)
|
|
||||||
Snackbar.make(
|
|
||||||
binding.recyclerViewChapters,
|
|
||||||
R.string.chapters_will_removed_background,
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_select_all -> {
|
|
||||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
|
||||||
selectionDecoration?.checkAll(ids)
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
mode.invalidate()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
val spinner = binding.spinnerBranches ?: return
|
|
||||||
viewModel.setSelectedBranch(spinner.selectedItem as String?)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_delete).isVisible = items.all { x ->
|
|
||||||
x.chapter.source == MangaSource.LOCAL
|
|
||||||
}
|
|
||||||
mode.title = items.size.toString()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
||||||
selectionDecoration?.clearSelection()
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
|
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
|
||||||
(item?.actionView as? SearchView)?.setQuery("", false)
|
|
||||||
viewModel.performChapterSearch(null)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
viewModel.performChapterSearch(newText)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.recyclerViewChapters.updatePadding(
|
|
||||||
bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSpinner(spinner: Spinner) {
|
|
||||||
val branchesAdapter = BranchesAdapter()
|
|
||||||
spinner.adapter = branchesAdapter
|
|
||||||
spinner.onItemSelectedListener = this
|
|
||||||
viewModel.branches.observe(viewLifecycleOwner) {
|
|
||||||
branchesAdapter.setItems(it)
|
|
||||||
spinner.isVisible = it.size > 1
|
|
||||||
}
|
|
||||||
viewModel.selectedBranchIndex.observe(viewLifecycleOwner) {
|
|
||||||
if (it != -1 && it != spinner.selectedItemPosition) {
|
|
||||||
spinner.setSelection(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onChaptersChanged(list: List<ChapterListItem>) {
|
|
||||||
val adapter = chaptersAdapter ?: return
|
|
||||||
if (adapter.itemCount == 0) {
|
|
||||||
val position = list.indexOfFirst { it.hasFlag(ChapterListItem.FLAG_CURRENT) } - 1
|
|
||||||
if (position > 0) {
|
|
||||||
val offset = (resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
|
|
||||||
adapter.setItems(list, RecyclerViewScrollCallback(binding.recyclerViewChapters, position, offset))
|
|
||||||
} else {
|
|
||||||
adapter.items = list
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
adapter.items = list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
|
||||||
binding.progressBar.isVisible = isLoading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import org.koin.core.parameter.parametersOf
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
|
||||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
|
||||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
|
||||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
|
||||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
|
||||||
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.getDisplayMessage
|
|
||||||
|
|
||||||
class DetailsActivity :
|
|
||||||
BaseActivity<ActivityDetailsBinding>(),
|
|
||||||
TabLayoutMediator.TabConfigurationStrategy,
|
|
||||||
AdapterView.OnItemSelectedListener {
|
|
||||||
|
|
||||||
private val viewModel by viewModel<DetailsViewModel> {
|
|
||||||
parametersOf(MangaIntent(intent))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val downloadReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
val downloadedManga = DownloadService.getDownloadedManga(intent) ?: return
|
|
||||||
viewModel.onDownloadComplete(downloadedManga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
|
|
||||||
supportActionBar?.run {
|
|
||||||
setDisplayHomeAsUpEnabled(true)
|
|
||||||
setDisplayShowTitleEnabled(false)
|
|
||||||
}
|
|
||||||
val pager = binding.pager
|
|
||||||
if (pager != null) {
|
|
||||||
pager.adapter = MangaDetailsAdapter(this)
|
|
||||||
TabLayoutMediator(checkNotNull(binding.tabs), pager, this).attach()
|
|
||||||
}
|
|
||||||
gcFragments()
|
|
||||||
binding.spinnerBranches?.let(::initSpinner)
|
|
||||||
|
|
||||||
viewModel.manga.observe(this, ::onMangaUpdated)
|
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
|
||||||
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
|
|
||||||
viewModel.onError.observe(this, ::onError)
|
|
||||||
|
|
||||||
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
unregisterReceiver(downloadReceiver)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaUpdated(manga: Manga) {
|
|
||||||
title = manga.title
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaRemoved(manga: Manga) {
|
|
||||||
Toast.makeText(
|
|
||||||
this, getString(R.string._s_deleted_from_local_storage, manga.title),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
finishAfterTransition()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
|
||||||
when {
|
|
||||||
ExceptionResolver.canResolve(e) -> {
|
|
||||||
resolveError(e)
|
|
||||||
}
|
|
||||||
viewModel.manga.value == null -> {
|
|
||||||
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
|
||||||
finishAfterTransition()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.snackbar.show(e.getDisplayMessage(resources))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.snackbar.updatePadding(
|
|
||||||
bottom = insets.bottom
|
|
||||||
)
|
|
||||||
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = insets.top
|
|
||||||
}
|
|
||||||
binding.root.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
right = insets.right
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNewChaptersChanged(newChapters: Int) {
|
|
||||||
val tab = binding.tabs?.getTabAt(1) ?: return
|
|
||||||
if (newChapters == 0) {
|
|
||||||
tab.removeBadge()
|
|
||||||
} else {
|
|
||||||
val badge = tab.orCreateBadge
|
|
||||||
badge.number = newChapters
|
|
||||||
badge.isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.opt_details, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
|
||||||
val manga = viewModel.manga.value
|
|
||||||
menu.findItem(R.id.action_save).isVisible =
|
|
||||||
manga?.source != null && manga.source != MangaSource.LOCAL
|
|
||||||
menu.findItem(R.id.action_delete).isVisible =
|
|
||||||
manga?.source == MangaSource.LOCAL
|
|
||||||
menu.findItem(R.id.action_browser).isVisible =
|
|
||||||
manga?.source != MangaSource.LOCAL
|
|
||||||
menu.findItem(R.id.action_shortcut).isVisible =
|
|
||||||
ShortcutManagerCompat.isRequestPinShortcutSupported(this)
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.action_share -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
if (it.source == MangaSource.LOCAL) {
|
|
||||||
ShareHelper(this).shareCbz(listOf(it.url.toUri().toFile()))
|
|
||||||
} else {
|
|
||||||
ShareHelper(this).shareMangaLink(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_delete -> {
|
|
||||||
val title = viewModel.manga.value?.title.orEmpty()
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.delete_manga)
|
|
||||||
.setMessage(getString(R.string.text_delete_local_manga, title))
|
|
||||||
.setPositiveButton(R.string.delete) { _, _ ->
|
|
||||||
viewModel.deleteLocal()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_save -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
val chaptersCount = it.chapters?.size ?: 0
|
|
||||||
val branches = viewModel.branches.value.orEmpty()
|
|
||||||
if (chaptersCount > 5 || branches.size > 1) {
|
|
||||||
showSaveConfirmation(it, chaptersCount, branches)
|
|
||||||
} else {
|
|
||||||
DownloadService.start(this, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_browser -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
startActivity(BrowserActivity.newIntent(this, it.publicUrl, it.title))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_related -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
startActivity(GlobalSearchActivity.newIntent(this, it.title))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_shortcut -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (!get<ShortcutsRepository>().requestPinShortcut(it)) {
|
|
||||||
binding.snackbar.show(getString(R.string.operation_not_supported))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
|
||||||
tab.text = when (position) {
|
|
||||||
0 -> getString(R.string.details)
|
|
||||||
1 -> getString(R.string.chapters)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
|
||||||
super.onSupportActionModeStarted(mode)
|
|
||||||
binding.pager?.isUserInputEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
|
||||||
super.onSupportActionModeFinished(mode)
|
|
||||||
binding.pager?.isUserInputEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
val spinner = binding.spinnerBranches ?: return
|
|
||||||
viewModel.setSelectedBranch(spinner.selectedItem as String?)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
|
|
||||||
fun showChapterMissingDialog(chapterId: Long) {
|
|
||||||
val remoteManga = viewModel.getRemoteManga()
|
|
||||||
if (remoteManga == null) {
|
|
||||||
binding.snackbar.show(getString(R.string.chapter_is_missing))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
MaterialAlertDialogBuilder(this).apply {
|
|
||||||
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(
|
|
||||||
context = this@DetailsActivity,
|
|
||||||
manga = remoteManga,
|
|
||||||
state = ReaderState(chapterId, 0, 0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setNeutralButton(R.string.download) { _, _ ->
|
|
||||||
DownloadService.start(this@DetailsActivity, remoteManga, setOf(chapterId))
|
|
||||||
}
|
|
||||||
setCancelable(true)
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSpinner(spinner: Spinner) {
|
|
||||||
val branchesAdapter = BranchesAdapter()
|
|
||||||
spinner.adapter = branchesAdapter
|
|
||||||
spinner.onItemSelectedListener = this
|
|
||||||
viewModel.branches.observe(this) {
|
|
||||||
branchesAdapter.setItems(it)
|
|
||||||
spinner.isVisible = it.size > 1
|
|
||||||
}
|
|
||||||
viewModel.selectedBranchIndex.observe(this) {
|
|
||||||
if (it != -1 && it != spinner.selectedItemPosition) {
|
|
||||||
spinner.setSelection(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveError(e: Throwable) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (exceptionResolver.resolve(e)) {
|
|
||||||
viewModel.reload()
|
|
||||||
} else if (viewModel.manga.value == null) {
|
|
||||||
Toast.makeText(this@DetailsActivity, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
|
|
||||||
finishAfterTransition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gcFragments() {
|
|
||||||
val mustHaveId = binding.pager == null
|
|
||||||
val fm = supportFragmentManager
|
|
||||||
val fragmentsToRemove = fm.fragments.filter { f ->
|
|
||||||
(f.id == 0) == mustHaveId
|
|
||||||
}
|
|
||||||
if (fragmentsToRemove.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fm.commit {
|
|
||||||
setReorderingAllowed(true)
|
|
||||||
for (f in fragmentsToRemove) {
|
|
||||||
remove(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<String?>) {
|
|
||||||
val dialogBuilder = MaterialAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.save_manga)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
if (branches.size > 1) {
|
|
||||||
val items = Array(branches.size) { i -> branches[i].orEmpty() }
|
|
||||||
val currentBranch = viewModel.selectedBranchIndex.value ?: -1
|
|
||||||
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
|
|
||||||
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
|
|
||||||
checkedIndices[i] = checked
|
|
||||||
}.setPositiveButton(R.string.save) { _, _ ->
|
|
||||||
val selectedBranches = branches.filterIndexedTo(HashSet()) { i, _ -> checkedIndices[i] }
|
|
||||||
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
|
|
||||||
if (c.branch in selectedBranches) c.id else null
|
|
||||||
}
|
|
||||||
DownloadService.start(this, manga, chaptersIds)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialogBuilder.setMessage(
|
|
||||||
getString(
|
|
||||||
R.string.large_manga_save_confirm,
|
|
||||||
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount)
|
|
||||||
)
|
|
||||||
).setPositiveButton(R.string.save) { _, _ ->
|
|
||||||
DownloadService.start(this, manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialogBuilder.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun newIntent(context: Context, manga: Manga): Intent {
|
|
||||||
return Intent(context, DetailsActivity::class.java)
|
|
||||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newIntent(context: Context, mangaId: Long): Intent {
|
|
||||||
return Intent(context, DetailsActivity::class.java)
|
|
||||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.*
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.util.CoilUtils
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
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.widgets.ChipsView
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
|
||||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
|
||||||
import org.koitharu.kotatsu.utils.FileSize
|
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
|
||||||
|
|
||||||
class DetailsFragment :
|
|
||||||
BaseFragment<FragmentDetailsBinding>(),
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
ChipsView.OnChipClickListener {
|
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
|
||||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInflateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
) = FragmentDetailsBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
binding.textViewAuthor.setOnClickListener(this)
|
|
||||||
binding.buttonFavorite.setOnClickListener(this)
|
|
||||||
binding.buttonRead.setOnClickListener(this)
|
|
||||||
binding.buttonRead.setOnLongClickListener(this)
|
|
||||||
binding.imageViewCover.setOnClickListener(this)
|
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
binding.chipsTags.onChipClickListener = this
|
|
||||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
|
||||||
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
|
|
||||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
inflater.inflate(R.menu.opt_details_info, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaUpdated(manga: Manga) {
|
|
||||||
with(binding) {
|
|
||||||
// Main
|
|
||||||
loadCover(manga)
|
|
||||||
textViewTitle.text = manga.title
|
|
||||||
textViewSubtitle.textAndVisible = manga.altTitle
|
|
||||||
textViewAuthor.textAndVisible = manga.author
|
|
||||||
textViewDescription.text = manga.description?.parseAsHtml()?.takeUnless(Spanned::isBlank)
|
|
||||||
?: getString(R.string.no_description)
|
|
||||||
when (manga.state) {
|
|
||||||
MangaState.FINISHED -> {
|
|
||||||
textViewState.apply {
|
|
||||||
textAndVisible = resources.getString(R.string.state_finished)
|
|
||||||
drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MangaState.ONGOING -> {
|
|
||||||
textViewState.apply {
|
|
||||||
textAndVisible = resources.getString(R.string.state_ongoing)
|
|
||||||
drawableStart = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> textViewState.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info containers
|
|
||||||
val chapters = manga.chapters
|
|
||||||
if (chapters.isNullOrEmpty()) {
|
|
||||||
infoLayout.textViewChapters.isVisible = false
|
|
||||||
} else {
|
|
||||||
infoLayout.textViewChapters.isVisible = true
|
|
||||||
infoLayout.textViewChapters.text = resources.getQuantityString(
|
|
||||||
R.plurals.chapters,
|
|
||||||
chapters.size,
|
|
||||||
chapters.size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (manga.hasRating) {
|
|
||||||
infoLayout.textViewRating.text = String.format("%.1f", manga.rating * 5)
|
|
||||||
infoLayout.ratingContainer.isVisible = true
|
|
||||||
} else {
|
|
||||||
infoLayout.ratingContainer.isVisible = false
|
|
||||||
}
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
infoLayout.textViewSource.isVisible = false
|
|
||||||
val file = manga.url.toUri().toFileOrNull()
|
|
||||||
if (file != null) {
|
|
||||||
viewLifecycleScope.launch {
|
|
||||||
val size = file.computeSize()
|
|
||||||
infoLayout.textViewSize.text = FileSize.BYTES.format(requireContext(), size)
|
|
||||||
infoLayout.textViewSize.isVisible = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
infoLayout.textViewSize.isVisible = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
infoLayout.textViewSource.text = manga.source.title
|
|
||||||
infoLayout.textViewSource.isVisible = true
|
|
||||||
infoLayout.textViewSize.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
infoLayout.textViewNsfw.isVisible = manga.isNsfw
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
|
|
||||||
|
|
||||||
// Chips
|
|
||||||
bindTags(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onHistoryChanged(history: MangaHistory?) {
|
|
||||||
with(binding.buttonRead) {
|
|
||||||
if (history == null) {
|
|
||||||
setText(R.string.read)
|
|
||||||
setIconResource(R.drawable.ic_read)
|
|
||||||
} else {
|
|
||||||
setText(R.string._continue)
|
|
||||||
setIconResource(R.drawable.ic_play)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFavouriteChanged(isFavourite: Boolean) {
|
|
||||||
val iconRes = if (isFavourite) {
|
|
||||||
R.drawable.ic_heart
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_heart_outline
|
|
||||||
}
|
|
||||||
binding.buttonFavorite.setIconResource(iconRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
|
||||||
if (isLoading) {
|
|
||||||
binding.progressBar.show()
|
|
||||||
} else {
|
|
||||||
binding.progressBar.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
val manga = viewModel.manga.value ?: return
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_favorite -> {
|
|
||||||
FavouriteCategoriesDialog.show(childFragmentManager, manga)
|
|
||||||
}
|
|
||||||
R.id.button_read -> {
|
|
||||||
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 = context ?: return,
|
|
||||||
manga = manga,
|
|
||||||
branch = viewModel.selectedBranchValue,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.textView_author -> {
|
|
||||||
startActivity(
|
|
||||||
SearchActivity.newIntent(
|
|
||||||
context = v.context,
|
|
||||||
source = manga.source,
|
|
||||||
query = manga.author ?: return,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.imageView_cover -> {
|
|
||||||
val options = ActivityOptions.makeSceneTransitionAnimation(
|
|
||||||
requireActivity(),
|
|
||||||
binding.imageViewCover,
|
|
||||||
binding.imageViewCover.transitionName,
|
|
||||||
)
|
|
||||||
startActivity(
|
|
||||||
ImageActivity.newIntent(v.context, manga.largeCoverUrl ?: manga.coverUrl),
|
|
||||||
options.toBundle()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean {
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_read -> {
|
|
||||||
if (viewModel.readingHistory.value == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val menu = PopupMenu(v.context, v)
|
|
||||||
menu.inflate(R.menu.popup_read)
|
|
||||||
menu.setOnMenuItemClickListener { menuItem ->
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.action_read -> {
|
|
||||||
val branch = viewModel.selectedBranchValue
|
|
||||||
startActivity(
|
|
||||||
ReaderActivity.newIntent(
|
|
||||||
context = context ?: return@setOnMenuItemClickListener false,
|
|
||||||
manga = viewModel.manga.value ?: return@setOnMenuItemClickListener false,
|
|
||||||
state = viewModel.chapters.value?.firstOrNull { c ->
|
|
||||||
c.chapter.branch == branch
|
|
||||||
}?.let { c ->
|
|
||||||
ReaderState(c.chapter.id, 0, 0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChipClick(chip: Chip, data: Any?) {
|
|
||||||
val tag = data as? MangaTag ?: return
|
|
||||||
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
|
||||||
binding.root.updatePadding(
|
|
||||||
bottom = insets.bottom,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindTags(manga: Manga) {
|
|
||||||
binding.chipsTags.setChips(
|
|
||||||
manga.tags.map { tag ->
|
|
||||||
ChipsView.ChipModel(
|
|
||||||
title = tag.title,
|
|
||||||
icon = 0,
|
|
||||||
data = tag,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadCover(manga: Manga) {
|
|
||||||
val currentCover = binding.imageViewCover.drawable
|
|
||||||
val request = ImageRequest.Builder(context ?: return)
|
|
||||||
.target(binding.imageViewCover)
|
|
||||||
if (currentCover != null) {
|
|
||||||
request.data(manga.largeCoverUrl ?: return)
|
|
||||||
.placeholderMemoryCacheKey(CoilUtils.metadata(binding.imageViewCover)?.memoryCacheKey)
|
|
||||||
.fallback(currentCover)
|
|
||||||
} else {
|
|
||||||
request.crossfade(true)
|
|
||||||
.data(manga.coverUrl)
|
|
||||||
.fallback(R.drawable.ic_placeholder)
|
|
||||||
}
|
|
||||||
request.referer(manga.publicUrl)
|
|
||||||
.lifecycle(viewLifecycleOwner)
|
|
||||||
.enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
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.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.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
|
||||||
import org.koitharu.kotatsu.utils.ext.iterator
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class DetailsViewModel(
|
|
||||||
private val intent: MangaIntent,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val favouritesRepository: FavouritesRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private var loadingJob: Job
|
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
|
||||||
private val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
private val history = mangaData.mapNotNull { it?.id }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.flatMapLatest { mangaId ->
|
|
||||||
historyRepository.observeOne(mangaId)
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
||||||
|
|
||||||
private val favourite = mangaData.mapNotNull { it?.id }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.flatMapLatest { mangaId ->
|
|
||||||
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val newChapters = mangaData.mapNotNull { it?.id }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.mapLatest { mangaId ->
|
|
||||||
trackingRepository.getNewChaptersCount(mangaId)
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
|
||||||
|
|
||||||
// Remote manga for saved and saved for remote
|
|
||||||
private val relatedManga = MutableStateFlow<Manga?>(null)
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
|
||||||
|
|
||||||
private val chaptersReversed = settings.observe()
|
|
||||||
.filter { it == AppSettings.KEY_REVERSE_CHAPTERS }
|
|
||||||
.map { settings.chaptersReverse }
|
|
||||||
.onStart { emit(settings.chaptersReverse) }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
val manga = mangaData.filterNotNull()
|
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val favouriteCategories = favourite
|
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val newChaptersCount = newChapters
|
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val readingHistory = history
|
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
val isChaptersReversed = chaptersReversed
|
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
|
||||||
|
|
||||||
val branches = mangaData.map {
|
|
||||||
it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty()
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val selectedBranchIndex = combine(
|
|
||||||
branches.asFlow(),
|
|
||||||
selectedBranch
|
|
||||||
) { branches, selected ->
|
|
||||||
branches.indexOf(selected)
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val isChaptersEmpty = mangaData.mapNotNull { m ->
|
|
||||||
m?.run { chapters.isNullOrEmpty() }
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
|
||||||
|
|
||||||
val chapters = combine(
|
|
||||||
combine(
|
|
||||||
mangaData.map { it?.chapters.orEmpty() },
|
|
||||||
relatedManga,
|
|
||||||
history.map { it?.chapterId },
|
|
||||||
newChapters,
|
|
||||||
selectedBranch
|
|
||||||
) { chapters, related, currentId, newCount, branch ->
|
|
||||||
val relatedChapters = related?.chapters
|
|
||||||
if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
||||||
mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch)
|
|
||||||
} else {
|
|
||||||
mapChapters(chapters, relatedChapters, currentId, newCount, branch)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
chaptersReversed,
|
|
||||||
chaptersQuery,
|
|
||||||
) { list, reversed, query ->
|
|
||||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
|
||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val selectedBranchValue: String?
|
|
||||||
get() = selectedBranch.value
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadingJob = doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reload() {
|
|
||||||
loadingJob.cancel()
|
|
||||||
loadingJob = doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteLocal() {
|
|
||||||
val m = mangaData.value ?: return
|
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
|
||||||
val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m)
|
|
||||||
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
|
|
||||||
val original = localMangaRepository.getRemoteManga(manga)
|
|
||||||
localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
|
|
||||||
runCatching {
|
|
||||||
historyRepository.deleteOrSwap(manga, original)
|
|
||||||
}
|
|
||||||
onMangaRemoved.postCall(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChaptersReversed(newValue: Boolean) {
|
|
||||||
settings.chaptersReverse = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedBranch(branch: String?) {
|
|
||||||
selectedBranch.value = branch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRemoteManga(): Manga? {
|
|
||||||
return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performChapterSearch(query: String?) {
|
|
||||||
chaptersQuery.value = query?.trim().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDownloadComplete(downloadedManga: Manga) {
|
|
||||||
val currentManga = mangaData.value ?: return
|
|
||||||
if (currentManga.id != downloadedManga.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentManga.source == MangaSource.LOCAL) {
|
|
||||||
reload()
|
|
||||||
} else {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
|
||||||
runCatching {
|
|
||||||
localMangaRepository.getDetails(downloadedManga)
|
|
||||||
}.onSuccess {
|
|
||||||
relatedManga.value = it
|
|
||||||
}.onFailure {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
it.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent)
|
|
||||||
?: throw MangaNotFoundException("Cannot find manga")
|
|
||||||
mangaData.value = manga
|
|
||||||
manga = MangaRepository(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = if (hist != null) {
|
|
||||||
val currentChapter = manga.chapters?.find { it.id == hist.chapterId }
|
|
||||||
if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters)
|
|
||||||
} else {
|
|
||||||
predictBranch(manga.chapters)
|
|
||||||
}
|
|
||||||
mangaData.value = manga
|
|
||||||
relatedManga.value = runCatching {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null
|
|
||||||
MangaRepository(m.source).getDetails(m)
|
|
||||||
} else {
|
|
||||||
localMangaRepository.findSavedManga(manga)
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
if (BuildConfig.DEBUG) error.printStackTrace()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChapters(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
downloadedChapters: List<MangaChapter>?,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val result = ArrayList<ChapterListItem>(chapters.size)
|
|
||||||
val dateFormat = settings.getDateFormat()
|
|
||||||
val currentIndex = chapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = chapters.size - newCount
|
|
||||||
val downloadedIds = downloadedChapters?.mapToSet { it.id }
|
|
||||||
for (i in chapters.indices) {
|
|
||||||
val chapter = chapters[i]
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = downloadedIds?.contains(chapter.id) == true,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapChaptersWithSource(
|
|
||||||
chapters: List<MangaChapter>,
|
|
||||||
sourceChapters: List<MangaChapter>,
|
|
||||||
currentId: Long?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id }
|
|
||||||
val result = ArrayList<ChapterListItem>(sourceChapters.size)
|
|
||||||
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
|
|
||||||
val firstNewIndex = sourceChapters.size - newCount
|
|
||||||
val dateFormat = settings.getDateFormat()
|
|
||||||
for (i in sourceChapters.indices) {
|
|
||||||
val chapter = sourceChapters[i]
|
|
||||||
val localChapter = chaptersMap.remove(chapter.id)
|
|
||||||
if (chapter.branch != branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result += localChapter?.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
) ?: chapter.toListItem(
|
|
||||||
isCurrent = i == currentIndex,
|
|
||||||
isUnread = i > currentIndex,
|
|
||||||
isNew = i >= firstNewIndex,
|
|
||||||
isMissing = true,
|
|
||||||
isDownloaded = false,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
|
|
||||||
result.ensureCapacity(result.size + chaptersMap.size)
|
|
||||||
chaptersMap.values.mapNotNullTo(result) {
|
|
||||||
if (it.branch == branch) {
|
|
||||||
it.toListItem(
|
|
||||||
isCurrent = false,
|
|
||||||
isUnread = true,
|
|
||||||
isNew = false,
|
|
||||||
isMissing = false,
|
|
||||||
isDownloaded = false,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.sortBy { it.chapter.number }
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun predictBranch(chapters: List<MangaChapter>?): String? {
|
|
||||||
if (chapters.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val groups = chapters.groupBy { it.branch }
|
|
||||||
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
|
||||||
var language = locale.getDisplayLanguage(locale).toTitleCase(locale)
|
|
||||||
if (groups.containsKey(language)) {
|
|
||||||
return language
|
|
||||||
}
|
|
||||||
language = locale.getDisplayName(locale).toTitleCase(locale)
|
|
||||||
if (groups.containsKey(language)) {
|
|
||||||
return language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups.maxByOrNull { it.value.size }?.key
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<ChapterListItem>.filterSearch(query: String): List<ChapterListItem> {
|
|
||||||
if (query.isEmpty() || this.isEmpty()) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return filter {
|
|
||||||
it.chapter.name.contains(query, ignoreCase = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
|
|
||||||
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
|
||||||
|
|
||||||
override fun getItemCount() = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment = when (position) {
|
|
||||||
0 -> DetailsFragment()
|
|
||||||
1 -> ChaptersFragment()
|
|
||||||
else -> throw IndexOutOfBoundsException("No fragment for position $position")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.TextView
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
|
||||||
|
|
||||||
class BranchesAdapter : BaseAdapter() {
|
|
||||||
|
|
||||||
private val dataSet = ArrayList<String?>()
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return dataSet.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return dataSet[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return dataSet[position].hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_branch, parent, false)
|
|
||||||
(view as TextView).text = dataSet[position]
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val view = convertView ?: LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.item_branch_dropdown, parent, false)
|
|
||||||
(view as TextView).text = dataSet[position]
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(items: Collection<String?>) {
|
|
||||||
dataSet.replaceWith(items)
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_UNREAD
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
|
||||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
|
||||||
|
|
||||||
fun chapterListItemAD(
|
|
||||||
clickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) = adapterDelegateViewBinding<ChapterListItem, ChapterListItem, ItemChapterBinding>(
|
|
||||||
{ inflater, parent -> ItemChapterBinding.inflate(inflater, parent, false) }
|
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = object : View.OnClickListener, View.OnLongClickListener {
|
|
||||||
override fun onClick(v: View) = clickListener.onItemClick(item, v)
|
|
||||||
override fun onLongClick(v: View) = clickListener.onItemLongClick(item, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setOnClickListener(eventListener)
|
|
||||||
itemView.setOnLongClickListener(eventListener)
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
binding.textViewTitle.text = item.chapter.name
|
|
||||||
binding.textViewNumber.text = item.chapter.number.toString()
|
|
||||||
binding.textViewDescription.textAndVisible = item.description()
|
|
||||||
}
|
|
||||||
when (item.status) {
|
|
||||||
FLAG_UNREAD -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_default)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorSecondaryInverse))
|
|
||||||
}
|
|
||||||
FLAG_CURRENT -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_accent)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorPrimaryInverse))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.textViewNumber.setBackgroundResource(R.drawable.bg_badge_outline)
|
|
||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val isMissing = item.hasFlag(FLAG_MISSING)
|
|
||||||
binding.textViewTitle.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewDescription.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
binding.textViewNumber.alpha = if (isMissing) 0.3f else 1f
|
|
||||||
|
|
||||||
binding.imageViewDownloaded.isVisible = item.hasFlag(FLAG_DOWNLOADED)
|
|
||||||
binding.imageViewNew.isVisible = item.hasFlag(FLAG_NEW)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.domain
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.IOException
|
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
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.PagesCache
|
|
||||||
import org.koitharu.kotatsu.local.domain.CbzMangaOutput
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
|
||||||
import org.koitharu.kotatsu.utils.ext.waitForNetwork
|
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressJob
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private const val MAX_DOWNLOAD_ATTEMPTS = 3
|
|
||||||
private const val DOWNLOAD_ERROR_DELAY = 500L
|
|
||||||
private const val SLOWDOWN_DELAY = 200L
|
|
||||||
|
|
||||||
class DownloadManager(
|
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val context: Context,
|
|
||||||
private val imageLoader: ImageLoader,
|
|
||||||
private val okHttp: OkHttpClient,
|
|
||||||
private val cache: PagesCache,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) {
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
private val semaphore = Semaphore(settings.downloadsParallelism)
|
|
||||||
|
|
||||||
fun downloadManga(
|
|
||||||
manga: Manga,
|
|
||||||
chaptersIds: LongArray?,
|
|
||||||
startId: Int,
|
|
||||||
): ProgressJob<DownloadState> {
|
|
||||||
val stateFlow = MutableStateFlow<DownloadState>(
|
|
||||||
DownloadState.Queued(startId = startId, manga = manga, cover = null)
|
|
||||||
)
|
|
||||||
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId)
|
|
||||||
return ProgressJob(job, stateFlow)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadMangaImpl(
|
|
||||||
manga: Manga,
|
|
||||||
chaptersIds: LongArray?,
|
|
||||||
outState: MutableStateFlow<DownloadState>,
|
|
||||||
startId: Int,
|
|
||||||
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
|
|
||||||
@Suppress("NAME_SHADOWING") var manga = manga
|
|
||||||
val chaptersIdsSet = chaptersIds?.toMutableSet()
|
|
||||||
val cover = loadCover(manga)
|
|
||||||
outState.value = DownloadState.Queued(startId, manga, cover)
|
|
||||||
localMangaRepository.lockManga(manga.id)
|
|
||||||
semaphore.acquire()
|
|
||||||
coroutineContext[WakeLockNode]?.acquire()
|
|
||||||
outState.value = DownloadState.Preparing(startId, manga, null)
|
|
||||||
val destination = localMangaRepository.getOutputDir()
|
|
||||||
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
|
|
||||||
val tempFileName = "${manga.id}_$startId.tmp"
|
|
||||||
var output: CbzMangaOutput? = null
|
|
||||||
try {
|
|
||||||
if (manga.source == MangaSource.LOCAL) {
|
|
||||||
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
|
|
||||||
}
|
|
||||||
val repo = MangaRepository(manga.source)
|
|
||||||
outState.value = DownloadState.Preparing(startId, manga, cover)
|
|
||||||
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
|
|
||||||
output = CbzMangaOutput.get(destination, data)
|
|
||||||
val coverUrl = data.largeCoverUrl ?: data.coverUrl
|
|
||||||
downloadFile(coverUrl, data.publicUrl, destination, tempFileName).let { file ->
|
|
||||||
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
|
|
||||||
}
|
|
||||||
val chapters = checkNotNull(
|
|
||||||
if (chaptersIdsSet == null) {
|
|
||||||
data.chapters
|
|
||||||
} else {
|
|
||||||
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
|
|
||||||
}
|
|
||||||
) { "Chapters list must not be null" }
|
|
||||||
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
|
|
||||||
check(chaptersIdsSet.isNullOrEmpty()) {
|
|
||||||
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
|
|
||||||
}
|
|
||||||
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
|
||||||
val pages = repo.getPages(chapter)
|
|
||||||
for ((pageIndex, page) in pages.withIndex()) {
|
|
||||||
var retryCounter = 0
|
|
||||||
failsafe@ while (true) {
|
|
||||||
try {
|
|
||||||
val url = repo.getPageUrl(page)
|
|
||||||
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
|
|
||||||
output.addPage(
|
|
||||||
chapter = chapter,
|
|
||||||
file = file,
|
|
||||||
pageNumber = pageIndex,
|
|
||||||
ext = MimeTypeMap.getFileExtensionFromUrl(url),
|
|
||||||
)
|
|
||||||
break@failsafe
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (retryCounter < MAX_DOWNLOAD_ATTEMPTS) {
|
|
||||||
outState.value = DownloadState.WaitingForNetwork(startId, data, cover)
|
|
||||||
delay(DOWNLOAD_ERROR_DELAY)
|
|
||||||
connectivityManager.waitForNetwork()
|
|
||||||
retryCounter++
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outState.value = DownloadState.Progress(
|
|
||||||
startId, data, cover,
|
|
||||||
totalChapters = chapters.size,
|
|
||||||
currentChapter = chapterIndex,
|
|
||||||
totalPages = pages.size,
|
|
||||||
currentPage = pageIndex,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (settings.isDownloadsSlowdownEnabled) {
|
|
||||||
delay(SLOWDOWN_DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outState.value = DownloadState.PostProcessing(startId, data, cover)
|
|
||||||
output.mergeWithExisting()
|
|
||||||
output.finalize()
|
|
||||||
val localManga = localMangaRepository.getFromFile(output.file)
|
|
||||||
outState.value = DownloadState.Done(startId, data, cover, localManga)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
outState.value = DownloadState.Cancelled(startId, manga, cover)
|
|
||||||
throw e
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
outState.value = DownloadState.Error(startId, manga, cover, e)
|
|
||||||
} finally {
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
output?.cleanup()
|
|
||||||
File(destination, tempFileName).deleteAwait()
|
|
||||||
}
|
|
||||||
coroutineContext[WakeLockNode]?.release()
|
|
||||||
semaphore.release()
|
|
||||||
localMangaRepository.unlockManga(manga.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun downloadFile(url: String, referer: String, destination: File, tempFileName: String): File {
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header(CommonHeaders.REFERER, referer)
|
|
||||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
val call = okHttp.newCall(request)
|
|
||||||
val file = File(destination, tempFileName)
|
|
||||||
val response = call.clone().await()
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
file.outputStream().use { out ->
|
|
||||||
checkNotNull(response.body).byteStream().copyTo(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun errorStateHandler(outState: MutableStateFlow<DownloadState>) =
|
|
||||||
CoroutineExceptionHandler { _, throwable ->
|
|
||||||
val prevValue = outState.value
|
|
||||||
outState.value = DownloadState.Error(
|
|
||||||
startId = prevValue.startId,
|
|
||||||
manga = prevValue.manga,
|
|
||||||
cover = prevValue.cover,
|
|
||||||
error = throwable,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadCover(manga: Manga) = runCatching {
|
|
||||||
imageLoader.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(manga.coverUrl)
|
|
||||||
.referer(manga.publicUrl)
|
|
||||||
.size(coverWidth, coverHeight)
|
|
||||||
.scale(Scale.FILL)
|
|
||||||
.build()
|
|
||||||
).drawable
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
class Factory(
|
|
||||||
private val context: Context,
|
|
||||||
private val imageLoader: ImageLoader,
|
|
||||||
private val okHttp: OkHttpClient,
|
|
||||||
private val cache: PagesCache,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(coroutineScope: CoroutineScope) = DownloadManager(
|
|
||||||
coroutineScope = coroutineScope,
|
|
||||||
context = context,
|
|
||||||
imageLoader = imageLoader,
|
|
||||||
okHttp = okHttp,
|
|
||||||
cache = cache,
|
|
||||||
localMangaRepository = localMangaRepository,
|
|
||||||
settings = settings,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.domain
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
sealed interface DownloadState {
|
|
||||||
|
|
||||||
val startId: Int
|
|
||||||
val manga: Manga
|
|
||||||
val cover: Drawable?
|
|
||||||
|
|
||||||
class Queued(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Queued
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Preparing(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Preparing
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
val max: Int = totalChapters * totalPages
|
|
||||||
|
|
||||||
val progress: Int = totalPages * currentChapter + currentPage + 1
|
|
||||||
|
|
||||||
val percent: Float = progress.toFloat() / max
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Progress
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
if (totalChapters != other.totalChapters) return false
|
|
||||||
if (currentChapter != other.currentChapter) return false
|
|
||||||
if (totalPages != other.totalPages) return false
|
|
||||||
if (currentPage != other.currentPage) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + totalChapters
|
|
||||||
result = 31 * result + currentChapter
|
|
||||||
result = 31 * result + totalPages
|
|
||||||
result = 31 * result + currentPage
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WaitingForNetwork(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as WaitingForNetwork
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Done(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
val localManga: Manga,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Done
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
if (localManga != other.localManga) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + localManga.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Error(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
val error: Throwable,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Error
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
if (error != other.error) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + error.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Cancelled(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Cancelled
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostProcessing(
|
|
||||||
override val startId: Int,
|
|
||||||
override val manga: Manga,
|
|
||||||
override val cover: Drawable?,
|
|
||||||
) : DownloadState {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as PostProcessing
|
|
||||||
|
|
||||||
if (startId != other.startId) return false
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (cover != other.cover) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = startId
|
|
||||||
result = 31 * result + manga.hashCode()
|
|
||||||
result = 31 * result + (cover?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user