Compare commits
1271 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a435435496 | ||
|
|
81e8c25563 | ||
|
|
e3504c3b1e | ||
|
|
2601c12348 | ||
|
|
138cf44e37 | ||
|
|
65d83e0921 | ||
|
|
6e1cd05fa8 | ||
|
|
8398c01929 | ||
|
|
835c49ae79 | ||
|
|
36065ccf6c | ||
|
|
4ab40566f7 | ||
|
|
bf01a4d1ab | ||
|
|
8dce9dcc3f | ||
|
|
d872044252 | ||
|
|
f4313525c2 | ||
|
|
4eb4ec7de0 | ||
|
|
ecb4dd87d9 | ||
|
|
3d0f5f75cd | ||
|
|
c5462e8454 | ||
|
|
5039e324fb | ||
|
|
b251b3e654 | ||
|
|
5f10070564 | ||
|
|
3da6f80eb6 | ||
|
|
4b2cfdb972 | ||
|
|
51387ace7e | ||
|
|
2bdb83ff28 | ||
|
|
a1b85433ec | ||
|
|
ca5207c658 | ||
|
|
81de6124f0 | ||
|
|
a93bc0ed5b | ||
|
|
a1b96ebbb5 | ||
|
|
6b93e49f56 | ||
|
|
c88a9dff36 | ||
|
|
ca47c475d3 | ||
|
|
8df7fa2729 | ||
|
|
ea34abb1d7 | ||
|
|
c4ff37350c | ||
|
|
95547a8d03 | ||
|
|
4c2197aa5d | ||
|
|
a679b6775d | ||
|
|
d3e4e97c6f | ||
|
|
d1b0af85c4 | ||
|
|
ce95e0657b | ||
|
|
6bb159a6d9 | ||
|
|
a75583f750 | ||
|
|
fff9df9609 | ||
|
|
f9609edea5 | ||
|
|
f1245742c0 | ||
|
|
42d933ba83 | ||
|
|
4df644e21f | ||
|
|
e4ba738c00 | ||
|
|
b7f09243aa | ||
|
|
50d4c41855 | ||
|
|
67adc8b681 | ||
|
|
34fb4af9fe | ||
|
|
05241f73d9 | ||
|
|
d666e4b967 | ||
|
|
b4bf607d3a | ||
|
|
a417d5aaa9 | ||
|
|
4b6b2c3e12 | ||
|
|
51300e30bd | ||
|
|
399ac07fb3 | ||
|
|
eeba161235 | ||
|
|
088a388812 | ||
|
|
943bba3ee8 | ||
|
|
18c3229200 | ||
|
|
9b6f511ac6 | ||
|
|
ad3b5dde91 | ||
|
|
ded7cdb71e | ||
|
|
74ca19a931 | ||
|
|
2684a7384e | ||
|
|
2c561824ef | ||
|
|
61e9796269 | ||
|
|
54597eb8f0 | ||
|
|
e07ea0552f | ||
|
|
b8e90719ce | ||
|
|
2ec716973c | ||
|
|
0000c97e6a | ||
|
|
70684de683 | ||
|
|
5a2288eb2d | ||
|
|
6a023fa976 | ||
|
|
3cc0fbe7bc | ||
|
|
b3b022807a | ||
|
|
ba0ea5a9fc | ||
|
|
ca1380e2b1 | ||
|
|
05dbd11fc1 | ||
|
|
aa650d44d3 | ||
|
|
99b698ad12 | ||
|
|
2f9c2d9ab6 | ||
|
|
ab753787b0 | ||
|
|
478ca351eb | ||
|
|
559b2cfd64 | ||
|
|
a1554f81ff | ||
|
|
4ce7f74b9d | ||
|
|
0778f34db7 | ||
|
|
bc488a6878 | ||
|
|
856524c9f8 | ||
|
|
c75160d83c | ||
|
|
d9c826524f | ||
|
|
40196205eb | ||
|
|
681ac492d6 | ||
|
|
95f4606661 | ||
|
|
b406834d4a | ||
|
|
2693ec5335 | ||
|
|
ead8a3d6df | ||
|
|
df04dcc8a3 | ||
|
|
c103773c19 | ||
|
|
0a28f131ee | ||
|
|
c8e4842b6e | ||
|
|
d54d489494 | ||
|
|
c77cb4cb3c | ||
|
|
d951306a90 | ||
|
|
c871757893 | ||
|
|
64bf671e8b | ||
|
|
b00bc22ead | ||
|
|
fdea2b47da | ||
|
|
91266183c2 | ||
|
|
20a7e5a6a8 | ||
|
|
6fa99791b6 | ||
|
|
4090c8ad6a | ||
|
|
c72ed7cc34 | ||
|
|
925c24471e | ||
|
|
7e31b1384e | ||
|
|
e1efb5fb1e | ||
|
|
29b5655efb | ||
|
|
68bdd22634 | ||
|
|
2e7867f60c | ||
|
|
e4c2972cae | ||
|
|
af654b8c40 | ||
|
|
9eace89d4f | ||
|
|
7dbec8eb8f | ||
|
|
111f816f18 | ||
|
|
d4589716aa | ||
|
|
1c4bd6da28 | ||
|
|
c83538f66d | ||
|
|
885cae7635 | ||
|
|
c66d36abf9 | ||
|
|
4bd9c6df81 | ||
|
|
b835cb98b7 | ||
|
|
7cb1f90155 | ||
|
|
96bac81b84 | ||
|
|
b59f933031 | ||
|
|
caebca36de | ||
|
|
03cb458d92 | ||
|
|
0788f5f05e | ||
|
|
0271ed2ba9 | ||
|
|
788c7b862a | ||
|
|
f4f84099cc | ||
|
|
3bea94bf1f | ||
|
|
746eed698f | ||
|
|
fa0289eb27 | ||
|
|
c874d73c04 | ||
|
|
edb91c46d4 | ||
|
|
4b9f4f9af2 | ||
|
|
a07117087a | ||
|
|
ce0ffca197 | ||
|
|
eece4d8f00 | ||
|
|
419e2e578b | ||
|
|
0a7387c22e | ||
|
|
2a23c3b3b3 | ||
|
|
0566aa4e6a | ||
|
|
3f27acf1aa | ||
|
|
d48c3fbe1b | ||
|
|
6720474667 | ||
|
|
ad42ca5085 | ||
|
|
d5e40d79ec | ||
|
|
7853b9a73e | ||
|
|
cb71a24d81 | ||
|
|
a5d99db105 | ||
|
|
6cc13784e4 | ||
|
|
cf9aab9afe | ||
|
|
83a919f9f6 | ||
|
|
70bf80ad8f | ||
|
|
05a724eae0 | ||
|
|
496f3637c4 | ||
|
|
c5c907c8dc | ||
|
|
af4845a770 | ||
|
|
511f9af991 | ||
|
|
baf6f6d2c9 | ||
|
|
8b6a0a8c87 | ||
|
|
e7ee261680 | ||
|
|
2949fdd2c6 | ||
|
|
829ea01b18 | ||
|
|
08b173b94a | ||
|
|
7b090c4ccd | ||
|
|
bf1b8e8b75 | ||
|
|
bfad632b8c | ||
|
|
73e6f730e1 | ||
|
|
62d8b848b2 | ||
|
|
2793f6ce52 | ||
|
|
b107801188 | ||
|
|
97de27dfb3 | ||
|
|
cd4317dec5 | ||
|
|
50554c6936 | ||
|
|
694297f49b | ||
|
|
3e48ce85fd | ||
|
|
0f7bceb268 | ||
|
|
00187c0d17 | ||
|
|
2378d104c3 | ||
|
|
f105f4b496 | ||
|
|
01e27ba91f | ||
|
|
2342594885 | ||
|
|
61a7f1c830 | ||
|
|
01c23bc3b8 | ||
|
|
7c7106a63c | ||
|
|
ac1a919476 | ||
|
|
234f74aa0d | ||
|
|
1711ebe616 | ||
|
|
07af79a6bd | ||
|
|
e4942b0d93 | ||
|
|
5ca22f1419 | ||
|
|
345a878d83 | ||
|
|
42bb5a65ab | ||
|
|
0c37265a5b | ||
|
|
7a65ae3ea7 | ||
|
|
376cee1859 | ||
|
|
ee027cd64f | ||
|
|
7b2bb5ea8f | ||
|
|
eff2d6bcb6 | ||
|
|
03b92c4898 | ||
|
|
6dcb537a9a | ||
|
|
052cfe26b1 | ||
|
|
45b2f2337a | ||
|
|
5785a2d5d1 | ||
|
|
bc273bfb8f | ||
|
|
513aa1a285 | ||
|
|
82a3b93214 | ||
|
|
80149b1ce7 | ||
|
|
297029a659 | ||
|
|
08acf2d882 | ||
|
|
dafca9e1e1 | ||
|
|
e174bc68af | ||
|
|
1d78c64350 | ||
|
|
321a9ecf62 | ||
|
|
83cf6aa997 | ||
|
|
e7bd74429e | ||
|
|
9ba87640c0 | ||
|
|
fff77cf208 | ||
|
|
967e8df7c9 | ||
|
|
f86d873361 | ||
|
|
2d5332d8df | ||
|
|
439a01c43f | ||
|
|
3a9d0def7d | ||
|
|
33a45ac5b3 | ||
|
|
e4c80b4443 | ||
|
|
940d448e00 | ||
|
|
5ab48a7545 | ||
|
|
cb2bdbdd9a | ||
|
|
8fdaf92cc4 | ||
|
|
0416077964 | ||
|
|
a8176e6589 | ||
|
|
a2437dd27a | ||
|
|
9e56766e9e | ||
|
|
eec750789d | ||
|
|
44a2b6db11 | ||
|
|
55ca2b8d8d | ||
|
|
2d670418c7 | ||
|
|
4c201bf950 | ||
|
|
7b60ed6bad | ||
|
|
619be69580 | ||
|
|
9f3c3f8985 | ||
|
|
f345977858 | ||
|
|
9610caf002 | ||
|
|
b75220a1b7 | ||
|
|
ab2a6f5a17 | ||
|
|
2aeefc607b | ||
|
|
9af769bc69 | ||
|
|
46b78cfcd7 | ||
|
|
c24324de9a | ||
|
|
48b9c1236d | ||
|
|
c69d293caa | ||
|
|
0f4cca0e07 | ||
|
|
d6500b8fec | ||
|
|
86140cab1e | ||
|
|
46ab5af905 | ||
|
|
9a815f28fa | ||
|
|
394479192b | ||
|
|
7908eb1441 | ||
|
|
ed672feebe | ||
|
|
4739da2774 | ||
|
|
fb674b6028 | ||
|
|
df5f5ea737 | ||
|
|
e6facd4e41 | ||
|
|
942d4fe5ab | ||
|
|
80db817ff2 | ||
|
|
b2817a2ce7 | ||
|
|
e2835e3e95 | ||
|
|
91928d058b | ||
|
|
425e8a49c4 | ||
|
|
148dbfdf02 | ||
|
|
55fdd6b7b1 | ||
|
|
a726b4f499 | ||
|
|
39cd199044 | ||
|
|
98b8aa3f2d | ||
|
|
f5b8d41a86 | ||
|
|
90dfc84119 | ||
|
|
f01fd18711 | ||
|
|
0098bdd07e | ||
|
|
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 | ||
|
|
9ded9b84e0 | ||
|
|
21037529c0 | ||
|
|
ec8bdbe6e1 | ||
|
|
b37fd6f4ab | ||
|
|
d853bb2c62 | ||
|
|
68dcacb918 | ||
|
|
49a7408715 | ||
|
|
5abbddba1e | ||
|
|
2382bf1063 | ||
|
|
f9b409634b | ||
|
|
bb96383d27 | ||
|
|
96a7a46981 | ||
|
|
4b3446ce0e | ||
|
|
6f302e2536 | ||
|
|
53c326ad05 | ||
|
|
7befb88e15 | ||
|
|
68e8ccf6bd | ||
|
|
a5b85c296a | ||
|
|
2d453bb553 | ||
|
|
ad1d247694 | ||
|
|
0077dc2f1c | ||
|
|
59a50e163f | ||
|
|
22e5b958bc | ||
|
|
905c4b362c | ||
|
|
6e71f20470 | ||
|
|
c326136bdb | ||
|
|
21c13835e6 | ||
|
|
deef3bd8b5 | ||
|
|
97884ae25b | ||
|
|
bc2ffef17e | ||
|
|
890c13d83a | ||
|
|
ec8d4b56b5 | ||
|
|
99cdfbc07b | ||
|
|
71efde08a8 | ||
|
|
678cef0a45 | ||
|
|
57f1a48602 | ||
|
|
f5eb1619ed | ||
|
|
032ed27c38 | ||
|
|
78f8407eca | ||
|
|
9a22001289 | ||
|
|
8e08b5003e | ||
|
|
e545c8b897 | ||
|
|
092a265e6d | ||
|
|
dc1f494e92 | ||
|
|
7b702e98da | ||
|
|
17d07f3b14 | ||
|
|
a21087ac9b | ||
|
|
0af1eebd62 | ||
|
|
39169d3afe | ||
|
|
c89ba12fb5 | ||
|
|
8fdec72f8d | ||
|
|
13119c95ac | ||
|
|
c70ecd9cfd | ||
|
|
6c43881cf4 | ||
|
|
57929f62ad | ||
|
|
523ee1e2a9 | ||
|
|
8b0f221eef | ||
|
|
656405edbc | ||
|
|
407c04fe2c | ||
|
|
3866d126b7 | ||
|
|
bdf836b7d9 | ||
|
|
997de528b8 | ||
|
|
0d8e4dee35 | ||
|
|
c313184666 | ||
|
|
ea3b43ba88 | ||
|
|
0eebddb24c | ||
|
|
6eb859b9f1 | ||
|
|
19f1322246 | ||
|
|
38f45ad483 | ||
|
|
7f46ff6d72 | ||
|
|
b9244bd11a | ||
|
|
532ec0129a | ||
|
|
464e47d6ad | ||
|
|
2bbdd3f044 | ||
|
|
0757a31381 | ||
|
|
36634414bc | ||
|
|
802448cb5a | ||
|
|
8ab9b4d1c3 | ||
|
|
02fa33597a | ||
|
|
5edfda6c1a | ||
|
|
5a565a16fe | ||
|
|
652ef7ee51 | ||
|
|
fb47480fba | ||
|
|
fc3ec644b3 | ||
|
|
9f21f5900f | ||
|
|
11710d36d1 | ||
|
|
a975ab58ee | ||
|
|
1306882377 | ||
|
|
b555e24fef | ||
|
|
b8250d5e44 | ||
|
|
9917787d6f | ||
|
|
d189e09aba | ||
|
|
7ff20245b3 | ||
|
|
da1696a059 | ||
|
|
e6202103bc | ||
|
|
7c659371a9 | ||
|
|
83886362be | ||
|
|
70de4f750c | ||
|
|
af901baff3 | ||
|
|
d69f4bbcaf | ||
|
|
089e3dc209 | ||
|
|
c158c4e18e | ||
|
|
300d365d8b | ||
|
|
81df005655 | ||
|
|
feb19c4eb5 | ||
|
|
9360787897 | ||
|
|
ac79557e22 | ||
|
|
c9695b1d2f | ||
|
|
27177996d3 | ||
|
|
2306330fd0 | ||
|
|
0d1e85d0c2 | ||
|
|
3b5a305122 | ||
|
|
e1db294b07 | ||
|
|
2004c3a7d5 | ||
|
|
44ac5270e0 | ||
|
|
f9baa4a8ad | ||
|
|
9c94a273ea | ||
|
|
efffbab4a7 | ||
|
|
7b53468c2e | ||
|
|
59243be030 | ||
|
|
57c1d070d1 | ||
|
|
5b2f2b0fd7 | ||
|
|
1a83d21410 | ||
|
|
094cebe674 | ||
|
|
3963099053 | ||
|
|
f3181cc0f1 | ||
|
|
008863fee8 | ||
|
|
1927008c2e | ||
|
|
5d0dac6947 | ||
|
|
71351ad701 | ||
|
|
abf5c8fb3c | ||
|
|
865311d864 | ||
|
|
4e8b8e0bc2 | ||
|
|
c0fbf846bd | ||
|
|
517f2bcee7 | ||
|
|
4607e5ba0f | ||
|
|
29c82177a9 | ||
|
|
69a0c779d9 | ||
|
|
3be9def609 | ||
|
|
bd3d800cde | ||
|
|
cef2449d45 | ||
|
|
8d894c97f3 | ||
|
|
0e1b5b19d2 | ||
|
|
2595c11686 | ||
|
|
334e08730e | ||
|
|
d6ff996dbe | ||
|
|
3040b89a9e | ||
|
|
aed180c845 | ||
|
|
4ba1e2f661 | ||
|
|
8b3ef3a3f3 | ||
|
|
b293fee742 | ||
|
|
2654de96ba | ||
|
|
e2aea345d4 | ||
|
|
78295898cb | ||
|
|
4fcdd9f370 | ||
|
|
9c66f74a5b | ||
|
|
9dc3ad38fc | ||
|
|
9599cdd2f6 | ||
|
|
4e5becb647 | ||
|
|
1d15a64945 | ||
|
|
21891d2d9d | ||
|
|
5b066c9dde | ||
|
|
e4668cf938 | ||
|
|
3978ebc230 | ||
|
|
4402db33fd | ||
|
|
3511c02c69 | ||
|
|
12be24c050 | ||
|
|
80db7f0b74 | ||
|
|
451b9fc0f1 | ||
|
|
e2ed7f0d77 | ||
|
|
4743f40154 | ||
|
|
53f127987c | ||
|
|
f6c6111459 | ||
|
|
f5dd1c39ce | ||
|
|
b519b53419 | ||
|
|
c5de765e52 | ||
|
|
1381a7d957 | ||
|
|
602a5eb2ab | ||
|
|
7d41318d15 | ||
|
|
dd8cb8dfd0 | ||
|
|
cfc317cf19 | ||
|
|
242704f853 | ||
|
|
6df56c2d77 | ||
|
|
49634a2f52 | ||
|
|
b7442fe445 | ||
|
|
b9428e3898 | ||
|
|
6a8a6a08db | ||
|
|
c4b03d1316 | ||
|
|
5d54298a22 | ||
|
|
11e9f1749a | ||
|
|
09105152e4 | ||
|
|
a2b8cfe512 | ||
|
|
f42f244443 | ||
|
|
b81aeaebd3 | ||
|
|
e0d93b0630 | ||
|
|
e4a2897731 | ||
|
|
eda72128da | ||
|
|
ebdc2dfb0e | ||
|
|
0eff85dca3 | ||
|
|
da5796b563 | ||
|
|
310d4e58bb | ||
|
|
455351e3a8 | ||
|
|
b4f2b82a0d | ||
|
|
0e7960fced | ||
|
|
1d2584001f | ||
|
|
d5216e3784 | ||
|
|
bd29c64370 | ||
|
|
a9f3ab259a | ||
|
|
318486d62b | ||
|
|
f5db5c39c3 | ||
|
|
7bc945b243 | ||
|
|
d31d302896 | ||
|
|
1be8760c00 | ||
|
|
32836d05d8 | ||
|
|
1a6b4ae795 | ||
|
|
fbc86f6d3b | ||
|
|
00e1aac984 | ||
|
|
830cc66933 | ||
|
|
8869bafe9e | ||
|
|
6ea98fa056 | ||
|
|
837fb91133 |
@@ -5,14 +5,15 @@ 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
|
||||||
|
ij_xml_attribute_wrap = on_every_item
|
||||||
|
|
||||||
[{*.kt,*.kts}]
|
[{*.kt,*.kts}]
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
|
|||||||
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"]
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
4
.github/ISSUE_TEMPLATE/report_bug.yml
vendored
@@ -61,4 +61,6 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
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
|
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
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,13 +7,16 @@
|
|||||||
/.idea/modules.xml
|
/.idea/modules.xml
|
||||||
/.idea/misc.xml
|
/.idea/misc.xml
|
||||||
/.idea/discord.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/androidTestResultsUserPreferences.xml
|
||||||
/.idea/render.experimental.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>
|
|
||||||
4
.idea/gradle.xml
generated
4
.idea/gradle.xml
generated
@@ -5,15 +5,15 @@
|
|||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
<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="Embedded 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>
|
||||||
|
|||||||
14
.idea/inspectionProfiles/Project_Default.xml
generated
14
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,14 +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="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
|
|
||||||
<option name="withoutDefaultValues" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<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>
|
||||||
3
.weblate
Normal file
3
.weblate
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[weblate]
|
||||||
|
url = https://hosted.weblate.org/api/
|
||||||
|
translation = kotatsu/strings
|
||||||
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## Kotatsu contribution guidelines
|
||||||
|
|
||||||
|
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/KotatsuApp/Kotatsu/issues), please, assign this issue to you and/or comment about it.
|
||||||
|
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
|
||||||
|
- Translations have to be managed using the [Weblate](https://hosted.weblate.org/engage/kotatsu/) platform.
|
||||||
|
- In case you want to add a new manga source, refer to the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers).
|
||||||
|
|
||||||
|
Refactoring or some dev-faces improvements are also might be accepted, however please stick to the following principles:
|
||||||
|
- Performance matters. In the case of choosing between source code beauty and performance, performance should be a priority.
|
||||||
|
- Please, do not modify readme and other information files (except for typos).
|
||||||
|
- Avoid adding new dependencies unless required. APK size is important.
|
||||||
22
README.md
22
README.md
@@ -2,30 +2,26 @@
|
|||||||
|
|
||||||
Kotatsu is a free and open source manga reader for Android.
|
Kotatsu is a free and open source manga reader for Android.
|
||||||
|
|
||||||
   [](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 directly from GitHub:
|
|
||||||
|
|
||||||
- **[Latest release](https://github.com/KotatsuApp/Kotatsu/releases/latest)**
|
|
||||||
|
|
||||||
### Main Features
|
### Main Features
|
||||||
|
|
||||||
* Online manga catalogues
|
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
|
||||||
* Search manga by name and genres
|
* Search manga by name and genres
|
||||||
* Reading history and bookmarks
|
* 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
|
||||||
* Shikimori integration (manga tracking)
|
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
|
||||||
* Password/fingerprint 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
|
||||||
|
|
||||||
@@ -43,6 +39,10 @@ Download APK directly from GitHub:
|
|||||||
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
|
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/)
|
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|||||||
144
app/build.gradle
144
app/build.gradle
@@ -2,32 +2,30 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
|
id 'com.google.devtools.ksp'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdk = 34
|
||||||
buildToolsVersion '32.0.0'
|
buildToolsVersion = '34.0.0'
|
||||||
namespace 'org.koitharu.kotatsu'
|
namespace = 'org.koitharu.kotatsu'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdk = 21
|
||||||
targetSdkVersion 32
|
targetSdk = 34
|
||||||
versionCode 422
|
versionCode = 580
|
||||||
versionName '3.4.10'
|
versionName = '6.1.2'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||||
|
ksp {
|
||||||
kapt {
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arguments {
|
}
|
||||||
arg 'room.schemaLocation', "$projectDir/schemas".toString()
|
androidResources {
|
||||||
}
|
generateLocaleConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
// define this values in your local.properties file
|
|
||||||
buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\""
|
|
||||||
buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\""
|
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
@@ -42,17 +40,20 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
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=kotlin.ExperimentalStdlibApi',
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
'-opt-in=kotlinx.coroutines.FlowPreview',
|
'-opt-in=kotlinx.coroutines.FlowPreview',
|
||||||
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||||
@@ -60,8 +61,8 @@ android {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError false
|
abortOnError true
|
||||||
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
|
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
|
||||||
}
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.includeAndroidResources true
|
unitTests.includeAndroidResources true
|
||||||
@@ -79,64 +80,81 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:f112a06ab6') {
|
//noinspection GradleDependency
|
||||||
|
implementation('com.github.KotatsuApp:kotatsu-parsers:7fbeb2e266') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.8.0'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
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.1'
|
||||||
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.1'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
implementation 'com.google.android.material:material:1.7.0-beta01'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'
|
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.4.3'
|
// TODO https://issuetracker.google.com/issues/254846063
|
||||||
implementation 'androidx.room:room-ktx:2.4.3'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
kapt 'androidx.room:room-compiler:2.4.3'
|
//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.10.0'
|
implementation 'androidx.room:room-runtime:2.5.2'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'androidx.room:room-ktx:2.5.2'
|
||||||
implementation 'com.squareup.okio:okio:3.2.0'
|
ksp '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.5.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.2.0'
|
implementation 'com.google.dagger:hilt-android:2.48'
|
||||||
implementation 'io.coil-kt:coil-base:2.2.0'
|
kapt 'com.google.dagger:hilt-compiler:2.48'
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
|
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:169806d928'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
implementation 'ch.acra:acra-mail:5.9.5'
|
implementation 'ch.acra:acra-http:5.11.2'
|
||||||
implementation 'ch.acra:acra-dialog:5.9.5'
|
implementation 'ch.acra:acra-dialog:5.11.2'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20220320'
|
testImplementation 'org.json:json:20230618'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
|
|
||||||
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 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
androidTestImplementation 'io.insert-koin:koin-test:3.2.0'
|
|
||||||
androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0'
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.4.3'
|
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||||
}
|
|
||||||
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48'
|
||||||
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48'
|
||||||
|
}
|
||||||
|
|||||||
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 { *; }
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
"sortKey": 1,
|
"sortKey": 1,
|
||||||
"order": "NEWEST",
|
"order": "NEWEST",
|
||||||
"createdAt": 1335906000000,
|
"createdAt": 1335906000000,
|
||||||
"isTrackingEnabled": true
|
"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.
@@ -1,67 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.settings.backup
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.koin.test.get
|
|
||||||
import org.koin.test.inject
|
|
||||||
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.domain.HistoryRepository
|
|
||||||
import kotlin.test.*
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class AppBackupAgentTest : KoinTest {
|
|
||||||
|
|
||||||
private val historyRepository by inject<HistoryRepository>()
|
|
||||||
private val favouritesRepository by inject<FavouritesRepository>()
|
|
||||||
private val backupRepository by inject<BackupRepository>()
|
|
||||||
private val database by inject<MangaDatabase>()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
database.clearAllTables()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testBackupRestore() = runTest {
|
|
||||||
val category = favouritesRepository.createCategory(
|
|
||||||
title = SampleData.favouriteCategory.title,
|
|
||||||
sortOrder = SampleData.favouriteCategory.order,
|
|
||||||
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
|
|
||||||
)
|
|
||||||
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(get(), 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))
|
|
||||||
assertContentEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
|
||||||
|
|
||||||
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
|
|
||||||
assertContains(allTags, SampleData.tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
|
||||||
|
class HiltTestRunner : AndroidJUnitRunner() {
|
||||||
|
|
||||||
|
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||||
|
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,4 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
|
|
||||||
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
|
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
|
||||||
waitForIdle { cont.resume(Unit) }
|
waitForIdle { cont.resume(Unit) }
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ package org.koitharu.kotatsu.core.db
|
|||||||
import androidx.room.testing.MigrationTestHelper
|
import androidx.room.testing.MigrationTestHelper
|
||||||
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 kotlin.test.assertEquals
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MangaDatabaseTest {
|
class MangaDatabaseTest {
|
||||||
@@ -17,7 +17,7 @@ class MangaDatabaseTest {
|
|||||||
MangaDatabase::class.java,
|
MangaDatabase::class.java,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val migrations = databaseMigrations
|
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun versions() {
|
fun versions() {
|
||||||
@@ -37,7 +37,7 @@ class MangaDatabaseTest {
|
|||||||
TEST_DB,
|
TEST_DB,
|
||||||
migration.endVersion,
|
migration.endVersion,
|
||||||
true,
|
true,
|
||||||
migration
|
migration,
|
||||||
).close()
|
).close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,28 +6,40 @@ import android.os.Build
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
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 dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.koin.test.inject
|
|
||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.awaitForIdle
|
import org.koitharu.kotatsu.awaitForIdle
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import kotlin.test.assertEquals
|
import javax.inject.Inject
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ShortcutsUpdaterTest : KoinTest {
|
class AppShortcutManagerTest {
|
||||||
|
|
||||||
private val historyRepository by inject<HistoryRepository>()
|
@get:Rule
|
||||||
private val shortcutsUpdater by inject<ShortcutsUpdater>()
|
var hiltRule = HiltAndroidRule(this)
|
||||||
private val database by inject<MangaDatabase>()
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appShortcutManager: AppShortcutManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var database: MangaDatabase
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
database.clearAllTables()
|
database.clearAllTables()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +48,7 @@ class ShortcutsUpdaterTest : KoinTest {
|
|||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
|
||||||
return@runTest
|
return@runTest
|
||||||
}
|
}
|
||||||
|
database.invalidationTracker.addObserver(appShortcutManager)
|
||||||
awaitUpdate()
|
awaitUpdate()
|
||||||
assertTrue(getShortcuts().isEmpty())
|
assertTrue(getShortcuts().isEmpty())
|
||||||
historyRepository.addOrUpdate(
|
historyRepository.addOrUpdate(
|
||||||
@@ -43,7 +56,7 @@ class ShortcutsUpdaterTest : KoinTest {
|
|||||||
chapterId = SampleData.chapter.id,
|
chapterId = SampleData.chapter.id,
|
||||||
page = 4,
|
page = 4,
|
||||||
scroll = 2,
|
scroll = 2,
|
||||||
percent = 0.3f
|
percent = 0.3f,
|
||||||
)
|
)
|
||||||
awaitUpdate()
|
awaitUpdate()
|
||||||
|
|
||||||
@@ -60,6 +73,6 @@ class ShortcutsUpdaterTest : KoinTest {
|
|||||||
private suspend fun awaitUpdate() {
|
private suspend fun awaitUpdate() {
|
||||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||||
instrumentation.awaitForIdle()
|
instrumentation.awaitForIdle()
|
||||||
shortcutsUpdater.await()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,39 @@
|
|||||||
package org.koitharu.kotatsu.tracker.domain
|
package org.koitharu.kotatsu.tracker.domain
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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 kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.koin.test.KoinTest
|
|
||||||
import org.koin.test.inject
|
|
||||||
import org.koitharu.kotatsu.SampleData
|
import org.koitharu.kotatsu.SampleData
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import kotlin.test.assertEquals
|
import javax.inject.Inject
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TrackerTest : KoinTest {
|
class TrackerTest {
|
||||||
|
|
||||||
private val repository by inject<TrackingRepository>()
|
@get:Rule
|
||||||
private val dataRepository by inject<MangaDataRepository>()
|
var hiltRule = HiltAndroidRule(this)
|
||||||
private val tracker by inject<Tracker>()
|
|
||||||
|
@Inject
|
||||||
|
lateinit var repository: TrackingRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataRepository: MangaDataRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var tracker: Tracker
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun noUpdates() = runTest {
|
fun noUpdates() = runTest {
|
||||||
@@ -180,4 +195,4 @@ class TrackerTest : KoinTest {
|
|||||||
dataRepository.storeManga(manga)
|
dataRepository.storeManga(manga)
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.utils.ext
|
|
||||||
|
|
||||||
fun Throwable.printStackTraceDebug() = printStackTrace()
|
|
||||||
45
app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
45
app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.StrictMode
|
||||||
|
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||||
|
import org.koitharu.kotatsu.core.BaseApp
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
|
|
||||||
|
class KotatsuApp : BaseApp() {
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
enableStrictMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.setClassInstanceLimit(PageLoader::class.java, 1)
|
||||||
|
.penaltyLog()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
||||||
|
.penaltyDeath()
|
||||||
|
.detectFragmentReuse()
|
||||||
|
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
|
||||||
|
.detectRetainInstanceUsage()
|
||||||
|
.detectSetUserVisibleHint()
|
||||||
|
.detectFragmentTagUsage()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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,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>
|
||||||
@@ -10,17 +10,46 @@
|
|||||||
<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.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.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<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: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"
|
||||||
@@ -43,6 +72,17 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
<action android:name="${applicationId}.action.VIEW_MANGA" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="kotatsu.app" />
|
||||||
|
<data android:path="/manga" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||||
@@ -56,35 +96,76 @@
|
|||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
|
||||||
android:label="@string/search_manga" />
|
android:exported="true"
|
||||||
|
android:label="@string/manga_list">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.action.EXPLORE_MANGA" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<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
|
||||||
|
android:name="org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity"
|
||||||
|
android:label="@string/related_manga" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/settings">
|
android:label="@string/settings">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="kotatsu" />
|
<data android:scheme="kotatsu" />
|
||||||
|
<data android:host="about" />
|
||||||
|
<data android:host="sync-settings" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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:autoRemoveFromRecents="true"
|
||||||
|
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.favourites.ui.categories.CategoriesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
||||||
android:label="@string/favourites_categories"
|
android:label="@string/manage_categories" />
|
||||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
|
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/manga_shelf"
|
android:label="@string/manga_shelf">
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetConfigActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/recent_manga">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -100,26 +181,86 @@
|
|||||||
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:label="@string/downloads"
|
android:label="@string/downloads"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop" />
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
|
||||||
<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
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
|
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
|
||||||
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
|
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.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
|
<service
|
||||||
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:stopWithTask="false"
|
android:foregroundServiceType="dataSync"
|
||||||
|
tools:node="merge" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
<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"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||||
<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"
|
||||||
@@ -134,6 +275,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"
|
||||||
@@ -157,6 +320,13 @@
|
|||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/widget_recent" />
|
android:resource="@xml/widget_recent" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name="org.koitharu.kotatsu.settings.about.UpdateDownloadReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
@@ -164,7 +334,664 @@
|
|||||||
<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" />
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name="org.koitharu.kotatsu.details.ui.DetailsBYLinkActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:targetActivity="org.koitharu.kotatsu.details.ui.DetailsActivity">
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="false">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="1stkissmanga.me" />
|
||||||
|
<data android:host="3asq.org" />
|
||||||
|
<data android:host="18porncomic.com" />
|
||||||
|
<data android:host="212.32.226.234" />
|
||||||
|
<data android:host="247manga.com" />
|
||||||
|
<data android:host="365manga.com" />
|
||||||
|
<data android:host="2023.allhen.online" />
|
||||||
|
<data android:host="adultwebtoon.com" />
|
||||||
|
<data android:host="afroditscans.com" />
|
||||||
|
<data android:host="ainzscans.site" />
|
||||||
|
<data android:host="aiyumanga.com" />
|
||||||
|
<data android:host="alceascan.my.id" />
|
||||||
|
<data android:host="allporncomic.com" />
|
||||||
|
<data android:host="anibel.net" />
|
||||||
|
<data android:host="anigliscans.com" />
|
||||||
|
<data android:host="anikiga.com" />
|
||||||
|
<data android:host="animaregia.net" />
|
||||||
|
<data android:host="anisamanga.com" />
|
||||||
|
<data android:host="anshscans.org" />
|
||||||
|
<data android:host="apenasmaisumyaoi.com" />
|
||||||
|
<data android:host="apollcomics.com" />
|
||||||
|
<data android:host="aquamanga.com" />
|
||||||
|
<data android:host="arabtoons.net" />
|
||||||
|
<data android:host="araznovel.com" />
|
||||||
|
<data android:host="arcanescans.com" />
|
||||||
|
<data android:host="arenascans.net" />
|
||||||
|
<data android:host="arthurscan.xyz" />
|
||||||
|
<data android:host="astral-manga.fr" />
|
||||||
|
<data android:host="astrallibrary.net" />
|
||||||
|
<data android:host="astrumscans.xyz" />
|
||||||
|
<data android:host="asura.nacm.xyz" />
|
||||||
|
<data android:host="asurascanstr.com" />
|
||||||
|
<data android:host="athenafansub.com" />
|
||||||
|
<data android:host="ayatoon.com" />
|
||||||
|
<data android:host="azoranov.com" />
|
||||||
|
<data android:host="azuremanga.com" />
|
||||||
|
<data android:host="babeltoon.com" />
|
||||||
|
<data android:host="bakai.org" />
|
||||||
|
<data android:host="bakaman.net" />
|
||||||
|
<data android:host="bakamh.com" />
|
||||||
|
<data android:host="banana-scan.com" />
|
||||||
|
<data android:host="bato.to" />
|
||||||
|
<data android:host="batocomic.com" />
|
||||||
|
<data android:host="batocomic.net" />
|
||||||
|
<data android:host="batocomic.org" />
|
||||||
|
<data android:host="batotoo.com" />
|
||||||
|
<data android:host="batotwo.com" />
|
||||||
|
<data android:host="battwo.com" />
|
||||||
|
<data android:host="beast-scans.com" />
|
||||||
|
<data android:host="beehentai.com" />
|
||||||
|
<data android:host="bentomanga.com" />
|
||||||
|
<data android:host="bestmanga.club" />
|
||||||
|
<data android:host="bestmanhua.com" />
|
||||||
|
<data android:host="bibimanga.com" />
|
||||||
|
<data android:host="birdmanga.com" />
|
||||||
|
<data android:host="birdtoon.net" />
|
||||||
|
<data android:host="blogmanga.net" />
|
||||||
|
<data android:host="blogtruyenmoi.com" />
|
||||||
|
<data android:host="bokugents.com" />
|
||||||
|
<data android:host="boosei.net" />
|
||||||
|
<data android:host="boyslove.me" />
|
||||||
|
<data android:host="br.atlantisscan.com" />
|
||||||
|
<data android:host="br.ninemanga.com" />
|
||||||
|
<data android:host="cabaredowatame.site" />
|
||||||
|
<data android:host="cafecomyaoi.com.br" />
|
||||||
|
<data android:host="carteldemanhwas.com" />
|
||||||
|
<data android:host="cat300.com" />
|
||||||
|
<data android:host="cerisescans.com" />
|
||||||
|
<data android:host="chap.mangairo.com" />
|
||||||
|
<data android:host="chapmanganato.com" />
|
||||||
|
<data android:host="chapmanganato.com" />
|
||||||
|
<data android:host="cizgiromanarsivi.com" />
|
||||||
|
<data android:host="cmreader.info" />
|
||||||
|
<data android:host="cocorip.net" />
|
||||||
|
<data android:host="coffeemanga.io" />
|
||||||
|
<data android:host="coloredmanga.com" />
|
||||||
|
<data android:host="comick.app" />
|
||||||
|
<data android:host="comiko.net" />
|
||||||
|
<data android:host="comiko.org" />
|
||||||
|
<data android:host="copypastescan.xyz" />
|
||||||
|
<data android:host="cosmicscans.com" />
|
||||||
|
<data android:host="daprob.com" />
|
||||||
|
<data android:host="darkscans.com" />
|
||||||
|
<data android:host="de.ninemanga.com" />
|
||||||
|
<data android:host="desu.me" />
|
||||||
|
<data android:host="diamondfansub.com" />
|
||||||
|
<data android:host="dojing.net" />
|
||||||
|
<data android:host="dokkomanga.com" />
|
||||||
|
<data android:host="dokkomanga.com" />
|
||||||
|
<data android:host="doujin69.com" />
|
||||||
|
<data android:host="doujindesu.rip" />
|
||||||
|
<data android:host="doujinhentai.net" />
|
||||||
|
<data android:host="dragontea.ink" />
|
||||||
|
<data android:host="dragontranslation.net" />
|
||||||
|
<data android:host="drakescans.com" />
|
||||||
|
<data android:host="dto.to" />
|
||||||
|
<data android:host="duckmanga.com" />
|
||||||
|
<data android:host="duniakomik.id" />
|
||||||
|
<data android:host="dynasty-scans.com" />
|
||||||
|
<data android:host="e-hentai.org" />
|
||||||
|
<data android:host="elarcpage.com" />
|
||||||
|
<data android:host="en.leviatanscans.com" />
|
||||||
|
<data android:host="epsilonscan.fr" />
|
||||||
|
<data android:host="es.ninemanga.com" />
|
||||||
|
<data android:host="esomanga.com" />
|
||||||
|
<data android:host="exhentai.org" />
|
||||||
|
<data android:host="falconmanga.com" />
|
||||||
|
<data android:host="fbsquads.com" />
|
||||||
|
<data android:host="finalscans.com" />
|
||||||
|
<data android:host="flamescans.org" />
|
||||||
|
<data android:host="foxwhite.com.br" />
|
||||||
|
<data android:host="fr-scan.cc" />
|
||||||
|
<data android:host="fr.ninemanga.com" />
|
||||||
|
<data android:host="franxxmangas.net" />
|
||||||
|
<data android:host="freakscans.com" />
|
||||||
|
<data android:host="freemanga.me" />
|
||||||
|
<data android:host="freemangatop.com" />
|
||||||
|
<data android:host="freewebtooncoins.com" />
|
||||||
|
<data android:host="frscans.com" />
|
||||||
|
<data android:host="furyosociety.com" />
|
||||||
|
<data android:host="galaxymanga.org" />
|
||||||
|
<data android:host="gatemanga.com" />
|
||||||
|
<data android:host="gdscans.com" />
|
||||||
|
<data android:host="gekkou.com.br" />
|
||||||
|
<data android:host="glorymanga.com" />
|
||||||
|
<data android:host="goldenmanga.top" />
|
||||||
|
<data android:host="golgebahcesi.com" />
|
||||||
|
<data android:host="gooffansub.com" />
|
||||||
|
<data android:host="gourmetscans.net" />
|
||||||
|
<data android:host="grabber.zone" />
|
||||||
|
<data android:host="gremorymangas.com" />
|
||||||
|
<data android:host="guimah.com" />
|
||||||
|
<data android:host="guncelmanga.net" />
|
||||||
|
<data android:host="h.mangabat.com" />
|
||||||
|
<data android:host="hachiraw.com" />
|
||||||
|
<data android:host="harimanga.com" />
|
||||||
|
<data android:host="hayalistic.com" />
|
||||||
|
<data android:host="hensekai.com" />
|
||||||
|
<data android:host="hentai3z.cc" />
|
||||||
|
<data android:host="hentai3z.xyz" />
|
||||||
|
<data android:host="hentai4free.net" />
|
||||||
|
<data android:host="hentai20.io" />
|
||||||
|
<data android:host="hentai.gekkouscans.com.br" />
|
||||||
|
<data android:host="hentai.scantrad-vf.cc" />
|
||||||
|
<data android:host="hentaichan.live" />
|
||||||
|
<data android:host="hentaichan.pro" />
|
||||||
|
<data android:host="hentaicube.net" />
|
||||||
|
<data android:host="hentailib.me" />
|
||||||
|
<data android:host="hentaimanga.me" />
|
||||||
|
<data android:host="hentaiteca.net" />
|
||||||
|
<data android:host="hentaivn.autos" />
|
||||||
|
<data android:host="hentaivn.tv" />
|
||||||
|
<data android:host="hentaiwebtoon.com" />
|
||||||
|
<data android:host="hentaixcomic.com" />
|
||||||
|
<data android:host="hentaixdickgirl.com" />
|
||||||
|
<data android:host="hentaixyuri.com" />
|
||||||
|
<data android:host="hentaizone.xyz" />
|
||||||
|
<data android:host="herenscan.com" />
|
||||||
|
<data android:host="hhentai.fr" />
|
||||||
|
<data android:host="hikariscan.com.br" />
|
||||||
|
<data android:host="hipercool.xyz" />
|
||||||
|
<data android:host="hmanhwa.com" />
|
||||||
|
<data android:host="hni-scantrad.com" />
|
||||||
|
<data android:host="honey-manga.com.ua" />
|
||||||
|
<data android:host="hscans.com" />
|
||||||
|
<data android:host="hto.to" />
|
||||||
|
<data android:host="id.gourmetscans.net" />
|
||||||
|
<data android:host="illusionscan.com" />
|
||||||
|
<data android:host="immortalupdates.com" />
|
||||||
|
<data android:host="immortalupdates.id" />
|
||||||
|
<data android:host="imperiodabritannia.com" />
|
||||||
|
<data android:host="imperioscans.com.br" />
|
||||||
|
<data android:host="indo18h.com" />
|
||||||
|
<data android:host="infrafandub.xyz" />
|
||||||
|
<data android:host="isekaiscan.top" />
|
||||||
|
<data android:host="it.ninemanga.com" />
|
||||||
|
<data android:host="itsyourightmanhua.com" />
|
||||||
|
<data android:host="jaiminisbox.net" />
|
||||||
|
<data android:host="japscan.ws" />
|
||||||
|
<data android:host="jiangzaitoon.co" />
|
||||||
|
<data android:host="jimanga.com" />
|
||||||
|
<data android:host="jpmangas.xyz" />
|
||||||
|
<data android:host="kanzenin.xyz" />
|
||||||
|
<data android:host="karatcam-scans.fr" />
|
||||||
|
<data android:host="katakomik.online" />
|
||||||
|
<data android:host="kiryuu.id" />
|
||||||
|
<data android:host="kissmanga.in" />
|
||||||
|
<data android:host="klikmanga.id" />
|
||||||
|
<data android:host="klz9.com" />
|
||||||
|
<data android:host="koinoboriscan.com" />
|
||||||
|
<data android:host="kolmanga.com" />
|
||||||
|
<data android:host="komikav.com" />
|
||||||
|
<data android:host="komikcast.io" />
|
||||||
|
<data android:host="komikdewasa.cfd" />
|
||||||
|
<data android:host="komikgo.org" />
|
||||||
|
<data android:host="komikhentai.co" />
|
||||||
|
<data android:host="komikid.com" />
|
||||||
|
<data android:host="komikindo.co" />
|
||||||
|
<data android:host="komikindo.info" />
|
||||||
|
<data android:host="komiklab.com" />
|
||||||
|
<data android:host="komiklokal.cfd" />
|
||||||
|
<data android:host="komikmama.co" />
|
||||||
|
<data android:host="komikmanhwa.me" />
|
||||||
|
<data android:host="komikmirror.art" />
|
||||||
|
<data android:host="komiksan.link" />
|
||||||
|
<data android:host="komiksay.site" />
|
||||||
|
<data android:host="komikstation.co" />
|
||||||
|
<data android:host="komiktap.in" />
|
||||||
|
<data android:host="komiku.com" />
|
||||||
|
<data android:host="komikzoid.xyz" />
|
||||||
|
<data android:host="ksgroupscans.com" />
|
||||||
|
<data android:host="kumascans.com" />
|
||||||
|
<data android:host="kunmanga.com" />
|
||||||
|
<data android:host="ladymanga.com" />
|
||||||
|
<data android:host="lectortmo.com" />
|
||||||
|
<data android:host="lectorunitoon.com" />
|
||||||
|
<data android:host="legacy-scans.com" />
|
||||||
|
<data android:host="legionscans.com" />
|
||||||
|
<data android:host="leitor.kamisama.com.br" />
|
||||||
|
<data android:host="leitorizakaya.net" />
|
||||||
|
<data android:host="lelscanvf.cc" />
|
||||||
|
<data android:host="leryaoi.com" />
|
||||||
|
<data android:host="lilymanga.net" />
|
||||||
|
<data android:host="limascans.xyz/v2" />
|
||||||
|
<data android:host="lkscanlation.com" />
|
||||||
|
<data android:host="lolicon.mobi" />
|
||||||
|
<data android:host="lugnica-scans.com" />
|
||||||
|
<data android:host="lunarscan.org" />
|
||||||
|
<data android:host="luxmanga.net" />
|
||||||
|
<data android:host="lxmanga.net" />
|
||||||
|
<data android:host="lynxscans.com" />
|
||||||
|
<data android:host="m.isekaiscan.to" />
|
||||||
|
<data android:host="mafia-manga.com" />
|
||||||
|
<data android:host="maidscan.com.br" />
|
||||||
|
<data android:host="manga1st.online" />
|
||||||
|
<data android:host="manga3s.com" />
|
||||||
|
<data android:host="manga18.club" />
|
||||||
|
<data android:host="manga68.com" />
|
||||||
|
<data android:host="manga689.com" />
|
||||||
|
<data android:host="manga-chan.me" />
|
||||||
|
<data android:host="manga-crab.com" />
|
||||||
|
<data android:host="manga-diyari.com" />
|
||||||
|
<data android:host="manga-fast.com" />
|
||||||
|
<data android:host="manga-fr.me" />
|
||||||
|
<data android:host="manga-mate.org" />
|
||||||
|
<data android:host="manga-moons.net" />
|
||||||
|
<data android:host="manga-scan.co" />
|
||||||
|
<data android:host="manga-scantrad.io" />
|
||||||
|
<data android:host="manga-tx.com" />
|
||||||
|
<data android:host="manga-uptocats.com" />
|
||||||
|
<data android:host="manga.clone-army.org" />
|
||||||
|
<data android:host="manga.in.ua" />
|
||||||
|
<data android:host="manga.mundodrama.site" />
|
||||||
|
<data android:host="mangaaction.com" />
|
||||||
|
<data android:host="mangaatrend.net" />
|
||||||
|
<data android:host="mangabaz.net" />
|
||||||
|
<data android:host="mangabob.com" />
|
||||||
|
<data android:host="mangabuddy.com" />
|
||||||
|
<data android:host="mangacim.com" />
|
||||||
|
<data android:host="mangaclash.com" />
|
||||||
|
<data android:host="mangacultivator.com" />
|
||||||
|
<data android:host="mangacute.com" />
|
||||||
|
<data android:host="mangacv.com" />
|
||||||
|
<data android:host="mangadass.com" />
|
||||||
|
<data android:host="mangadeemak.com" />
|
||||||
|
<data android:host="mangadex.org" />
|
||||||
|
<data android:host="mangadistrict.com" />
|
||||||
|
<data android:host="mangadna.com" />
|
||||||
|
<data android:host="mangadoor.com" />
|
||||||
|
<data android:host="mangaeffect.com" />
|
||||||
|
<data android:host="mangaforest.me" />
|
||||||
|
<data android:host="mangaforfree.com" />
|
||||||
|
<data android:host="mangafoxfull.com" />
|
||||||
|
<data android:host="mangafreak.online" />
|
||||||
|
<data android:host="mangagalaxy.me" />
|
||||||
|
<data android:host="mangagg.com" />
|
||||||
|
<data android:host="mangagoyaoi.com" />
|
||||||
|
<data android:host="mangagreat.com" />
|
||||||
|
<data android:host="mangahentai.me" />
|
||||||
|
<data android:host="mangahub.fr" />
|
||||||
|
<data android:host="mangaid.click" />
|
||||||
|
<data android:host="mangaindo.me" />
|
||||||
|
<data android:host="mangak2.com" />
|
||||||
|
<data android:host="mangakakalot.com" />
|
||||||
|
<data android:host="mangakeyfi.net" />
|
||||||
|
<data android:host="mangaking.net" />
|
||||||
|
<data android:host="mangakio.me" />
|
||||||
|
<data android:host="mangakiss.org" />
|
||||||
|
<data android:host="mangakita.net" />
|
||||||
|
<data android:host="mangakomi.io" />
|
||||||
|
<data android:host="mangakyo.org" />
|
||||||
|
<data android:host="mangalek.com" />
|
||||||
|
<data android:host="mangaleks.com" />
|
||||||
|
<data android:host="mangaleveling.com" />
|
||||||
|
<data android:host="mangalib.me" />
|
||||||
|
<data android:host="mangalike.me" />
|
||||||
|
<data android:host="mangalink.online" />
|
||||||
|
<data android:host="mangalionz.com" />
|
||||||
|
<data android:host="mangamammy.ru" />
|
||||||
|
<data android:host="mangamanhua.online" />
|
||||||
|
<data android:host="mangamaniacs.org" />
|
||||||
|
<data android:host="manganato.com" />
|
||||||
|
<data android:host="mangaokutr.com" />
|
||||||
|
<data android:host="mangaonelove.site" />
|
||||||
|
<data android:host="mangaonlineteam.com" />
|
||||||
|
<data android:host="mangaowl.to" />
|
||||||
|
<data android:host="mangaprotm.com" />
|
||||||
|
<data android:host="mangapt.com" />
|
||||||
|
<data android:host="mangapuma.com" />
|
||||||
|
<data android:host="mangaread.co" />
|
||||||
|
<data android:host="mangareaderpro.com" />
|
||||||
|
<data android:host="mangareading.org" />
|
||||||
|
<data android:host="mangarockteam.com" />
|
||||||
|
<data android:host="mangarocky.com" />
|
||||||
|
<data android:host="mangarolls.net" />
|
||||||
|
<data android:host="mangarosie.in" />
|
||||||
|
<data android:host="mangas-origines.fr" />
|
||||||
|
<data android:host="mangas-origines.xyz" />
|
||||||
|
<data android:host="mangaschan.com" />
|
||||||
|
<data android:host="mangasehri.com" />
|
||||||
|
<data android:host="mangaspark.com" />
|
||||||
|
<data android:host="mangastarz.com" />
|
||||||
|
<data android:host="mangastic.cc" />
|
||||||
|
<data android:host="mangastic.cc" />
|
||||||
|
<data android:host="mangasushi.org" />
|
||||||
|
<data android:host="mangasusuku.xyz" />
|
||||||
|
<data android:host="mangatale.co" />
|
||||||
|
<data android:host="mangatone.com" />
|
||||||
|
<data android:host="mangatoto.com" />
|
||||||
|
<data android:host="mangatoto.net" />
|
||||||
|
<data android:host="mangatoto.org" />
|
||||||
|
<data android:host="mangatx.com" />
|
||||||
|
<data android:host="mangaus.xyz" />
|
||||||
|
<data android:host="mangavisa.com" />
|
||||||
|
<data android:host="mangaweebs.in" />
|
||||||
|
<data android:host="mangawt.com" />
|
||||||
|
<data android:host="mangax1.com" />
|
||||||
|
<data android:host="mangaxyz.com" />
|
||||||
|
<data android:host="mangayaro.net" />
|
||||||
|
<data android:host="mangazavr.ru" />
|
||||||
|
<data android:host="mangazodiac.com" />
|
||||||
|
<data android:host="manhatic.com" />
|
||||||
|
<data android:host="manhuaes.com" />
|
||||||
|
<data android:host="manhuafast.com" />
|
||||||
|
<data android:host="manhuafast.net" />
|
||||||
|
<data android:host="manhuaga.com" />
|
||||||
|
<data android:host="manhuahot.com" />
|
||||||
|
<data android:host="manhuamix.com" />
|
||||||
|
<data android:host="manhuaplus.com" />
|
||||||
|
<data android:host="manhuascan.us" />
|
||||||
|
<data android:host="manhuaus.com" />
|
||||||
|
<data android:host="manhuazone.net" />
|
||||||
|
<data android:host="manhwa18.app" />
|
||||||
|
<data android:host="manhwa18.com" />
|
||||||
|
<data android:host="manhwa18.net" />
|
||||||
|
<data android:host="manhwa18.org" />
|
||||||
|
<data android:host="manhwa68.com" />
|
||||||
|
<data android:host="manhwa-latino.com" />
|
||||||
|
<data android:host="manhwaclan.com" />
|
||||||
|
<data android:host="manhwadesu.top" />
|
||||||
|
<data android:host="manhwafull.com" />
|
||||||
|
<data android:host="manhwahentai.me" />
|
||||||
|
<data android:host="manhwaindo.icu" />
|
||||||
|
<data android:host="manhwaindo.id" />
|
||||||
|
<data android:host="manhwakool.com" />
|
||||||
|
<data android:host="manhwalist.xyz" />
|
||||||
|
<data android:host="manhwalover.com" />
|
||||||
|
<data android:host="manhwaplus.pro" />
|
||||||
|
<data android:host="manhwasco.net" />
|
||||||
|
<data android:host="manhwatop.com" />
|
||||||
|
<data android:host="manhwaworld.com" />
|
||||||
|
<data android:host="manhwax.org" />
|
||||||
|
<data android:host="manhwaz.com" />
|
||||||
|
<data android:host="mantrazscan.com" />
|
||||||
|
<data android:host="manwe.pro" />
|
||||||
|
<data android:host="manycomic.com" />
|
||||||
|
<data android:host="manytoon.com" />
|
||||||
|
<data android:host="manytoon.me" />
|
||||||
|
<data android:host="masterkomik.com" />
|
||||||
|
<data android:host="melokomik.xyz" />
|
||||||
|
<data android:host="mgkomik.com" />
|
||||||
|
<data android:host="miauscans.com" />
|
||||||
|
<data android:host="milftoon.xxx" />
|
||||||
|
<data android:host="mintmanga.com" />
|
||||||
|
<data android:host="mintmanga.live" />
|
||||||
|
<data android:host="mirrordesu.ink" />
|
||||||
|
<data android:host="mm-scans.org" />
|
||||||
|
<data android:host="momonohanascan.com" />
|
||||||
|
<data android:host="monarcamanga.com" />
|
||||||
|
<data android:host="moonloversscan.com.br" />
|
||||||
|
<data android:host="moonwitchinlovescan.com" />
|
||||||
|
<data android:host="mortalsgroove.com" />
|
||||||
|
<data android:host="mto.to" />
|
||||||
|
<data android:host="mundomangakun.com.br" />
|
||||||
|
<data android:host="mundomanhwa.com" />
|
||||||
|
<data android:host="murimscan.run" />
|
||||||
|
<data android:host="neatmangas.com" />
|
||||||
|
<data android:host="neoxscans.net" />
|
||||||
|
<data android:host="nettruyenin.com" />
|
||||||
|
<data android:host="nettruyento.com" />
|
||||||
|
<data android:host="neumanga.net" />
|
||||||
|
<data android:host="neumanga.xyz" />
|
||||||
|
<data android:host="nhattruyenmin.com" />
|
||||||
|
<data android:host="nhentai.net" />
|
||||||
|
<data android:host="nicovideo.jp" />
|
||||||
|
<data android:host="nightscans.org" />
|
||||||
|
<data android:host="niji-translations.com" />
|
||||||
|
<data android:host="ninjascan.site" />
|
||||||
|
<data android:host="niverafansub.com" />
|
||||||
|
<data android:host="nocsummer.com.br" />
|
||||||
|
<data android:host="noindexscan.com" />
|
||||||
|
<data android:host="nonbiri.space" />
|
||||||
|
<data android:host="novelcrow.com" />
|
||||||
|
<data android:host="novelmic.com" />
|
||||||
|
<data android:host="novelstown.cyou" />
|
||||||
|
<data android:host="nude-moon.net" />
|
||||||
|
<data android:host="nude-moon.org" />
|
||||||
|
<data android:host="nyxmanga.com" />
|
||||||
|
<data android:host="origami-orpheans.com.br" />
|
||||||
|
<data android:host="otsugami.id" />
|
||||||
|
<data android:host="oxapk.com" />
|
||||||
|
<data android:host="ozulmanga.com" />
|
||||||
|
<data android:host="painfulnightz.com" />
|
||||||
|
<data android:host="pantheon-scan.com" />
|
||||||
|
<data android:host="papscan.com" />
|
||||||
|
<data android:host="paragonscans.com" />
|
||||||
|
<data android:host="peacescans.com" />
|
||||||
|
<data android:host="phantomscans.com" />
|
||||||
|
<data android:host="phenixscans.fr" />
|
||||||
|
<data android:host="pianmanga.me" />
|
||||||
|
<data android:host="pirulitorosa.site" />
|
||||||
|
<data android:host="piscans.in" />
|
||||||
|
<data android:host="platinumscans.com" />
|
||||||
|
<data android:host="pojokmanga.net" />
|
||||||
|
<data android:host="popsmanga.com" />
|
||||||
|
<data android:host="portalyaoi.com" />
|
||||||
|
<data android:host="prismahentai.com" />
|
||||||
|
<data android:host="prismascans.net" />
|
||||||
|
<data android:host="projetoscanlator.com" />
|
||||||
|
<data android:host="psunicorn.com" />
|
||||||
|
<data android:host="queenscans.com" />
|
||||||
|
<data android:host="ragnarokscan.com" />
|
||||||
|
<data android:host="ragnarokscanlation.com" />
|
||||||
|
<data android:host="raijinscans.fr" />
|
||||||
|
<data android:host="raikiscan.com" />
|
||||||
|
<data android:host="rainbowfairyscan.com" />
|
||||||
|
<data android:host="randomscans.com" />
|
||||||
|
<data android:host="ravenscans.com" />
|
||||||
|
<data android:host="rawdex.net" />
|
||||||
|
<data android:host="rawkuma.com" />
|
||||||
|
<data android:host="read-nifteam.info" />
|
||||||
|
<data android:host="read.babelwuxia.com" />
|
||||||
|
<data android:host="readcomicsonline.ru" />
|
||||||
|
<data android:host="reader.deathtollscans.net" />
|
||||||
|
<data android:host="reader.decadencescans.com" />
|
||||||
|
<data android:host="reader.evilflowers.com" />
|
||||||
|
<data android:host="reader.mangatellers.gr" />
|
||||||
|
<data android:host="reader.onepiecenakama.pl" />
|
||||||
|
<data android:host="reader.powermanga.org" />
|
||||||
|
<data android:host="reader.silentsky-scans.net" />
|
||||||
|
<data android:host="readfreecomics.com" />
|
||||||
|
<data android:host="readkomik.com" />
|
||||||
|
<data android:host="readmanga.io" />
|
||||||
|
<data android:host="readmanga.live" />
|
||||||
|
<data android:host="readmanga.me" />
|
||||||
|
<data android:host="readmangabat.com" />
|
||||||
|
<data android:host="readmanhua.net" />
|
||||||
|
<data android:host="readtoto.com" />
|
||||||
|
<data android:host="readtoto.net" />
|
||||||
|
<data android:host="readtoto.org" />
|
||||||
|
<data android:host="realmscans.xyz" />
|
||||||
|
<data android:host="reaperscans.fr" />
|
||||||
|
<data android:host="remanga.org" />
|
||||||
|
<data android:host="rightdark-scan.com" />
|
||||||
|
<data android:host="rio2manga.com" />
|
||||||
|
<data android:host="rio2manga.net" />
|
||||||
|
<data android:host="rogmangas.com" />
|
||||||
|
<data android:host="romantikmanga.com" />
|
||||||
|
<data android:host="ru.ninemanga.com" />
|
||||||
|
<data android:host="s2manga.com" />
|
||||||
|
<data android:host="samuraiscan.com" />
|
||||||
|
<data android:host="sawamics.com" />
|
||||||
|
<data android:host="saytruyenhay.com" />
|
||||||
|
<data android:host="scambertraslator.com" />
|
||||||
|
<data android:host="scan.hentai.menu" />
|
||||||
|
<data android:host="scanmanga-vf.ws" />
|
||||||
|
<data android:host="scansmangas.me" />
|
||||||
|
<data android:host="scansraw.com" />
|
||||||
|
<data android:host="scantrad-union.com" />
|
||||||
|
<data android:host="scantrad-vf.co" />
|
||||||
|
<data android:host="sekaikomik.pro" />
|
||||||
|
<data android:host="sektedoujin.cc" />
|
||||||
|
<data android:host="sektekomik.xyz" />
|
||||||
|
<data android:host="selfmanga.live" />
|
||||||
|
<data android:host="senpaiediciones.com" />
|
||||||
|
<data android:host="shadowmangas.com" />
|
||||||
|
<data android:host="shadowtrad.net" />
|
||||||
|
<data android:host="sheakomik.com" />
|
||||||
|
<data android:host="shibamanga.com" />
|
||||||
|
<data android:host="shinigami.id" />
|
||||||
|
<data android:host="shirodoujin.com" />
|
||||||
|
<data android:host="shootingstarscans.com" />
|
||||||
|
<data android:host="silencescan.com.br" />
|
||||||
|
<data android:host="sinensisscans.com" />
|
||||||
|
<data android:host="skanlacje-feniksy.pl" />
|
||||||
|
<data android:host="skymanga.work" />
|
||||||
|
<data android:host="skymangas.com" />
|
||||||
|
<data android:host="sleepytranslations.com" />
|
||||||
|
<data android:host="soulscans.my.id" />
|
||||||
|
<data android:host="spartanmanga.com.tr" />
|
||||||
|
<data android:host="sssscanlator.com" />
|
||||||
|
<data android:host="summanga.com" />
|
||||||
|
<data android:host="suryascans.com" />
|
||||||
|
<data android:host="sushiscan.fr" />
|
||||||
|
<data android:host="sushiscan.net" />
|
||||||
|
<data android:host="swatop.club" />
|
||||||
|
<data android:host="tankouhentai.com" />
|
||||||
|
<data android:host="tatakaescan.com" />
|
||||||
|
<data android:host="tecnoscann.com" />
|
||||||
|
<data android:host="teenmanhua.com" />
|
||||||
|
<data android:host="tempestfansub.com" />
|
||||||
|
<data android:host="templescan.net" />
|
||||||
|
<data android:host="templescanesp.com" />
|
||||||
|
<data android:host="tenkaiscan.net" />
|
||||||
|
<data android:host="theguildscans.com" />
|
||||||
|
<data android:host="thesugarscan.com" />
|
||||||
|
<data android:host="timenaight.com" />
|
||||||
|
<data android:host="todaymic.com" />
|
||||||
|
<data android:host="tonizutoon.com" />
|
||||||
|
<data android:host="toonchill.com" />
|
||||||
|
<data android:host="toonfr.com" />
|
||||||
|
<data android:host="toonhunter.com" />
|
||||||
|
<data android:host="toonily.com" />
|
||||||
|
<data android:host="toonily.me" />
|
||||||
|
<data android:host="toonily.net" />
|
||||||
|
<data android:host="toonitube.com" />
|
||||||
|
<data android:host="tortuga-ceviri.com" />
|
||||||
|
<data android:host="traduccionesmoonlight.com" />
|
||||||
|
<data android:host="treemanga.com" />
|
||||||
|
<data android:host="tritinia.org" />
|
||||||
|
<data android:host="truemanga.com" />
|
||||||
|
<data android:host="truyentranhlh.net" />
|
||||||
|
<data android:host="tsundoku.com.br" />
|
||||||
|
<data android:host="tukangkomik.id" />
|
||||||
|
<data android:host="tumanhwas.club" />
|
||||||
|
<data android:host="turktoon.com" />
|
||||||
|
<data android:host="v2.comiz.net" />
|
||||||
|
<data android:host="valkyriescan.com" />
|
||||||
|
<data android:host="vercomicsporno.com" />
|
||||||
|
<data android:host="vermangasporno.com" />
|
||||||
|
<data android:host="vermanhwa.es" />
|
||||||
|
<data android:host="viyafansub.com" />
|
||||||
|
<data android:host="void-scans.com" />
|
||||||
|
<data android:host="w.mangairo.com" />
|
||||||
|
<data android:host="wakamics.net" />
|
||||||
|
<data android:host="webcomic.me" />
|
||||||
|
<data android:host="webtoon-tr.com" />
|
||||||
|
<data android:host="webtoon.uk" />
|
||||||
|
<data android:host="webtoonempire.org" />
|
||||||
|
<data android:host="webtoonhatti.com" />
|
||||||
|
<data android:host="webtoons.top" />
|
||||||
|
<data android:host="webtoonscan.com" />
|
||||||
|
<data android:host="weloma.art" />
|
||||||
|
<data android:host="welovemanga.one" />
|
||||||
|
<data android:host="westmanga.info" />
|
||||||
|
<data android:host="wickedwitchscan.com" />
|
||||||
|
<data android:host="winterscan.com" />
|
||||||
|
<data android:host="wonderlandscan.com" />
|
||||||
|
<data android:host="woopread.com" />
|
||||||
|
<data android:host="worldmanhwas.bar" />
|
||||||
|
<data android:host="wto.to" />
|
||||||
|
<data android:host="www1.bluesolo.org" />
|
||||||
|
<data android:host="www.areascans.net" />
|
||||||
|
<data android:host="www.bentomanga.com" />
|
||||||
|
<data android:host="www.eromiau.com" />
|
||||||
|
<data android:host="www.inu-manga.com" />
|
||||||
|
<data android:host="www.japscan.lol" />
|
||||||
|
<data android:host="www.kuroimanga.com" />
|
||||||
|
<data android:host="www.lami-manga.com" />
|
||||||
|
<data android:host="www.lelmanga.com" />
|
||||||
|
<data android:host="www.lianscans.my.id" />
|
||||||
|
<data android:host="www.maid.my.id" />
|
||||||
|
<data android:host="www.majorscans.com" />
|
||||||
|
<data android:host="www.mangadods.com" />
|
||||||
|
<data android:host="www.mangaread.org" />
|
||||||
|
<data android:host="www.mangascantrad.fr" />
|
||||||
|
<data android:host="www.mangatown.com" />
|
||||||
|
<data android:host="www.manhuabug.com" />
|
||||||
|
<data android:host="www.manhuakey.com" />
|
||||||
|
<data android:host="www.manhuasy.com" />
|
||||||
|
<data android:host="www.menudo-fansub.com" />
|
||||||
|
<data android:host="www.nettruyenmax.com" />
|
||||||
|
<data android:host="www.nettruyento.com" />
|
||||||
|
<data android:host="www.nightcomic.com" />
|
||||||
|
<data android:host="www.ninemanga.com" />
|
||||||
|
<data android:host="www.noblessetranslations.com" />
|
||||||
|
<data android:host="www.pantheon-scan.fr" />
|
||||||
|
<data android:host="www.paritehaber.com" />
|
||||||
|
<data android:host="www.peachscan.com" />
|
||||||
|
<data android:host="www.petrotechsociety.org" />
|
||||||
|
<data android:host="www.petrotechsociety.org" />
|
||||||
|
<data android:host="www.ramareader.it" />
|
||||||
|
<data android:host="www.rh2plusmanga.com" />
|
||||||
|
<data android:host="www.ruyamanga.com" />
|
||||||
|
<data android:host="www.scan-fr.org" />
|
||||||
|
<data android:host="www.scan-vf.net" />
|
||||||
|
<data android:host="www.thaimanga.net" />
|
||||||
|
<data android:host="www.topmanhua.com" />
|
||||||
|
<data android:host="www.vfscan.com" />
|
||||||
|
<data android:host="www.walpurgiscan.it" />
|
||||||
|
<data android:host="www.webtoon.xyz" />
|
||||||
|
<data android:host="www.witcomics.net" />
|
||||||
|
<data android:host="www.xn--l3c0azab5a2gta.com" />
|
||||||
|
<data android:host="www.yaoitoshokan.net" />
|
||||||
|
<data android:host="xbato.com" />
|
||||||
|
<data android:host="xbato.net" />
|
||||||
|
<data android:host="xbato.org" />
|
||||||
|
<data android:host="xoxocomics.net" />
|
||||||
|
<data android:host="xx.hentaichan.live" />
|
||||||
|
<data android:host="xxx.hentaichan.live" />
|
||||||
|
<data android:host="y.hentaichan.live" />
|
||||||
|
<data android:host="yaoi-chan.me" />
|
||||||
|
<data android:host="yaoi.mobi" />
|
||||||
|
<data android:host="yaoilib.me" />
|
||||||
|
<data android:host="yaoiscan.com" />
|
||||||
|
<data android:host="ycscan.com" />
|
||||||
|
<data android:host="yugenmangas.com.br" />
|
||||||
|
<data android:host="yuri.live" />
|
||||||
|
<data android:host="zahard.xyz" />
|
||||||
|
<data android:host="zandynofansub.aishiteru.org" />
|
||||||
|
<data android:host="zbato.com" />
|
||||||
|
<data android:host="zbato.net" />
|
||||||
|
<data android:host="zbato.org" />
|
||||||
|
<data android:host="zeroscan.com.br" />
|
||||||
|
<data android:host="zinmanga.com" />
|
||||||
|
<data android:host="zinmanhwa.com" />
|
||||||
|
<data android:host="zuttomanga.com" />
|
||||||
|
<data android:host="реманга.орг" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
package org.koitharu.kotatsu
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.StrictMode
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
|
||||||
import androidx.room.InvalidationTracker
|
|
||||||
import org.acra.ReportField
|
|
||||||
import org.acra.config.dialog
|
|
||||||
import org.acra.config.mailSender
|
|
||||||
import org.acra.data.StringFormat
|
|
||||||
import org.acra.ktx.initAcra
|
|
||||||
import org.koin.android.ext.android.get
|
|
||||||
import org.koin.android.ext.android.getKoin
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koitharu.kotatsu.bookmarks.bookmarksModule
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
||||||
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.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.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.reader.readerModule
|
|
||||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.shikimoriModule
|
|
||||||
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.appWidgetModule
|
|
||||||
|
|
||||||
class KotatsuApp : Application() {
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
enableStrictMode()
|
|
||||||
}
|
|
||||||
initKoin()
|
|
||||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
|
||||||
setupActivityLifecycleCallbacks()
|
|
||||||
setupDatabaseObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initKoin() {
|
|
||||||
startKoin {
|
|
||||||
androidContext(this@KotatsuApp)
|
|
||||||
modules(
|
|
||||||
networkModule,
|
|
||||||
databaseModule,
|
|
||||||
githubModule,
|
|
||||||
uiModule,
|
|
||||||
mainModule,
|
|
||||||
searchModule,
|
|
||||||
localModule,
|
|
||||||
favouritesModule,
|
|
||||||
historyModule,
|
|
||||||
remoteListModule,
|
|
||||||
detailsModule,
|
|
||||||
trackerModule,
|
|
||||||
settingsModule,
|
|
||||||
readerModule,
|
|
||||||
appWidgetModule,
|
|
||||||
suggestionsModule,
|
|
||||||
shikimoriModule,
|
|
||||||
bookmarksModule
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
|
||||||
super.attachBaseContext(base)
|
|
||||||
initAcra {
|
|
||||||
buildConfigClass = BuildConfig::class.java
|
|
||||||
reportFormat = StringFormat.KEY_VALUE_LIST
|
|
||||||
reportContent = listOf(
|
|
||||||
ReportField.PACKAGE_NAME,
|
|
||||||
ReportField.APP_VERSION_CODE,
|
|
||||||
ReportField.APP_VERSION_NAME,
|
|
||||||
ReportField.ANDROID_VERSION,
|
|
||||||
ReportField.PHONE_MODEL,
|
|
||||||
ReportField.CRASH_CONFIGURATION,
|
|
||||||
ReportField.STACK_TRACE,
|
|
||||||
ReportField.SHARED_PREFERENCES
|
|
||||||
)
|
|
||||||
dialog {
|
|
||||||
text = getString(R.string.crash_text)
|
|
||||||
title = getString(R.string.error_occurred)
|
|
||||||
positiveButtonText = getString(R.string.send)
|
|
||||||
resIcon = R.drawable.ic_alert_outline
|
|
||||||
resTheme = android.R.style.Theme_Material_Light_Dialog_Alert
|
|
||||||
}
|
|
||||||
mailSender {
|
|
||||||
mailTo = getString(R.string.email_error_report)
|
|
||||||
reportAsFile = true
|
|
||||||
reportFileName = "stacktrace.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupDatabaseObservers() {
|
|
||||||
val observers = getKoin().getAll<InvalidationTracker.Observer>()
|
|
||||||
val database = get<MangaDatabase>()
|
|
||||||
val tracker = database.invalidationTracker
|
|
||||||
observers.forEach {
|
|
||||||
tracker.addObserver(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupActivityLifecycleCallbacks() {
|
|
||||||
val callbacks = getKoin().getAll<ActivityLifecycleCallbacks>()
|
|
||||||
callbacks.forEach {
|
|
||||||
registerActivityLifecycleCallbacks(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,76 +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.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 java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
object MangaUtils : KoinComponent {
|
|
||||||
|
|
||||||
private const val MIN_WEBTOON_RATIO = 2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatic determine type of manga by page size
|
|
||||||
* @return ReaderMode.WEBTOON if page is wide
|
|
||||||
*/
|
|
||||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean {
|
|
||||||
val pageIndex = (pages.size * 0.3).roundToInt()
|
|
||||||
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
|
|
||||||
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 * MIN_WEBTOON_RATIO < size.height
|
|
||||||
}
|
|
||||||
|
|
||||||
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,19 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.domain
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
|
||||||
|
|
||||||
fun interface ReversibleHandle {
|
|
||||||
|
|
||||||
suspend fun reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle {
|
|
||||||
this.reverse()
|
|
||||||
other.reverse()
|
|
||||||
}
|
|
||||||
@@ -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,86 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams
|
|
||||||
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 org.koitharu.kotatsu.utils.ext.displayCompat
|
|
||||||
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
|
|
||||||
|
|
||||||
// Enforce max width for tablets
|
|
||||||
val width = resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
|
|
||||||
if (width > 0) {
|
|
||||||
behavior?.maxWidth = width
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set peek height to 50% display height
|
|
||||||
requireContext().displayCompat?.let {
|
|
||||||
val metrics = DisplayMetrics()
|
|
||||||
it.getRealMetrics(metrics)
|
|
||||||
behavior?.peekHeight = (metrics.heightPixels * 0.4).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
viewBinding = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return AppBottomSheetDialog(requireContext(), theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBottomSheetCallback(callback: BottomSheetBehavior.BottomSheetCallback) {
|
|
||||||
val b = behavior ?: return
|
|
||||||
b.addBottomSheetCallback(callback)
|
|
||||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
|
||||||
if (rootView != null) {
|
|
||||||
callback.onStateChanged(rootView, b.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,59 +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
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
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
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
|
||||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO WindowInsetsControllerCompat works incorrect
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun hideSystemUI() {
|
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
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,49 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData
|
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
abstract class BaseViewModel : ViewModel() {
|
|
||||||
|
|
||||||
protected val loadingCounter = CountedBooleanLiveData()
|
|
||||||
protected val errorEvent = SingleLiveEvent<Throwable>()
|
|
||||||
|
|
||||||
val onError: LiveData<Throwable>
|
|
||||||
get() = errorEvent
|
|
||||||
|
|
||||||
val isLoading: LiveData<Boolean>
|
|
||||||
get() = loadingCounter
|
|
||||||
|
|
||||||
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) {
|
|
||||||
loadingCounter.increment()
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
loadingCounter.decrement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
|
||||||
throwable.printStackTraceDebug()
|
|
||||||
if (throwable !is CancellationException) {
|
|
||||||
errorEvent.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,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.dialog
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.View
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
|
|
||||||
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://github.com/material-components/material-components-android/issues/2582
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
val window = window
|
|
||||||
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
if (window != null) {
|
|
||||||
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
|
|
||||||
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
|
|
||||||
if (drawEdgeToEdge) {
|
|
||||||
// Copied from super.onAttachedToWindow:
|
|
||||||
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
// Fix super-class's window flag bug by respecting the initial system UI visibility:
|
|
||||||
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,87 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.content.res.getColorOrThrow
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
abstract class AbstractDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val bounds = Rect()
|
|
||||||
private val thickness: Int
|
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
||||||
|
|
||||||
init {
|
|
||||||
paint.style = Paint.Style.FILL
|
|
||||||
val ta = context.obtainStyledAttributes(
|
|
||||||
null,
|
|
||||||
materialR.styleable.MaterialDivider,
|
|
||||||
materialR.attr.materialDividerStyle,
|
|
||||||
materialR.style.Widget_Material3_MaterialDivider,
|
|
||||||
)
|
|
||||||
paint.color = ta.getColorOrThrow(materialR.styleable.MaterialDivider_dividerColor)
|
|
||||||
thickness = ta.getDimensionPixelSize(
|
|
||||||
materialR.styleable.MaterialDivider_dividerThickness,
|
|
||||||
context.resources.getDimensionPixelSize(materialR.dimen.material_divider_thickness),
|
|
||||||
)
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State,
|
|
||||||
) {
|
|
||||||
outRect.set(0, thickness, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement for horizontal lists on demand
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
|
|
||||||
if (parent.layoutManager == null || thickness == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
canvas.save()
|
|
||||||
val left: Float
|
|
||||||
val right: Float
|
|
||||||
if (parent.clipToPadding) {
|
|
||||||
left = parent.paddingLeft.toFloat()
|
|
||||||
right = (parent.width - parent.paddingRight).toFloat()
|
|
||||||
canvas.clipRect(
|
|
||||||
left,
|
|
||||||
parent.paddingTop.toFloat(),
|
|
||||||
right,
|
|
||||||
(parent.height - parent.paddingBottom).toFloat()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
left = 0f
|
|
||||||
right = parent.width.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
var previous: RecyclerView.ViewHolder? = null
|
|
||||||
for (child in parent.children) {
|
|
||||||
val holder = parent.getChildViewHolder(child)
|
|
||||||
if (previous != null && shouldDrawDivider(previous, holder)) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, bounds)
|
|
||||||
val top: Float = bounds.top + child.translationY
|
|
||||||
val bottom: Float = top + thickness
|
|
||||||
canvas.drawRect(left, top, right, bottom, paint)
|
|
||||||
}
|
|
||||||
previous = holder
|
|
||||||
}
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun shouldDrawDivider(
|
|
||||||
above: RecyclerView.ViewHolder,
|
|
||||||
below: RecyclerView.ViewHolder,
|
|
||||||
): Boolean
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list.decor
|
|
||||||
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.util.SparseIntArray
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.util.getOrDefault
|
|
||||||
import androidx.core.util.set
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class TypedSpacingItemDecoration(
|
|
||||||
vararg spacingMapping: Pair<Int, Int>,
|
|
||||||
private val fallbackSpacing: Int = 0,
|
|
||||||
) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
private val mapping = SparseIntArray(spacingMapping.size)
|
|
||||||
|
|
||||||
init {
|
|
||||||
spacingMapping.forEach { (k, v) -> mapping[k] = v }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
val itemType = parent.getChildViewHolder(view)?.itemViewType
|
|
||||||
val spacing = if (itemType == null) {
|
|
||||||
fallbackSpacing
|
|
||||||
} else {
|
|
||||||
mapping.getOrDefault(itemType, fallbackSpacing)
|
|
||||||
}
|
|
||||||
outRect.set(spacing, spacing, spacing, spacing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.util
|
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class CountedBooleanLiveData : LiveData<Boolean>(false) {
|
|
||||||
|
|
||||||
private val counter = AtomicInteger(0)
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun increment() {
|
|
||||||
if (counter.getAndIncrement() == 0) {
|
|
||||||
postValue(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun decrement() {
|
|
||||||
if (counter.decrementAndGet() == 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AnyThread
|
|
||||||
fun reset() {
|
|
||||||
if (counter.getAndSet(0) != 0) {
|
|
||||||
postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,133 +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.content.res.ColorStateList
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import org.koitharu.kotatsu.databinding.FadingSnackbarLayoutBinding
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColorStateList
|
|
||||||
import com.google.android.material.R as materialR
|
|
||||||
|
|
||||||
private const val ENTER_DURATION = 300L
|
|
||||||
private const val EXIT_DURATION = 200L
|
|
||||||
private const val SHORT_DURATION_MS = 1_500L
|
|
||||||
private const val LONG_DURATION_MS = 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 binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.snackbarLayout.background = createThemedBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismiss() {
|
|
||||||
if (visibility == VISIBLE && alpha == 1f) {
|
|
||||||
animate()
|
|
||||||
.alpha(0f)
|
|
||||||
.withEndAction { visibility = GONE }
|
|
||||||
.duration = EXIT_DURATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(
|
|
||||||
messageText: CharSequence?,
|
|
||||||
@StringRes actionId: Int = 0,
|
|
||||||
duration: Int = Snackbar.LENGTH_SHORT,
|
|
||||||
onActionClick: (FadingSnackbar.() -> Unit)? = null,
|
|
||||||
onDismiss: (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
binding.snackbarText.text = messageText
|
|
||||||
if (actionId != 0) {
|
|
||||||
with(binding.snackbarAction) {
|
|
||||||
visibility = VISIBLE
|
|
||||||
text = context.getString(actionId)
|
|
||||||
setOnClickListener {
|
|
||||||
onActionClick?.invoke(this@FadingSnackbar) ?: dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.snackbarAction.visibility = GONE
|
|
||||||
}
|
|
||||||
alpha = 0f
|
|
||||||
visibility = VISIBLE
|
|
||||||
animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.duration = ENTER_DURATION
|
|
||||||
if (duration == Snackbar.LENGTH_INDEFINITE) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val durationMs = ENTER_DURATION + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS
|
|
||||||
postDelayed(durationMs) {
|
|
||||||
dismiss()
|
|
||||||
onDismiss?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createThemedBackground(): Drawable {
|
|
||||||
val backgroundColor = MaterialColors.layer(this, materialR.attr.colorSurface, materialR.attr.colorOnSurface, 1f)
|
|
||||||
val shapeAppearanceModel = ShapeAppearanceModel.builder(
|
|
||||||
context,
|
|
||||||
materialR.style.ShapeAppearance_Material3_Corner_ExtraSmall,
|
|
||||||
0
|
|
||||||
).build()
|
|
||||||
val background = createMaterialShapeDrawableBackground(
|
|
||||||
backgroundColor,
|
|
||||||
shapeAppearanceModel,
|
|
||||||
)
|
|
||||||
val backgroundTint = context.getThemeColorStateList(materialR.attr.colorSurfaceInverse)
|
|
||||||
return if (backgroundTint != null) {
|
|
||||||
val wrappedDrawable = DrawableCompat.wrap(background)
|
|
||||||
DrawableCompat.setTintList(wrappedDrawable, backgroundTint)
|
|
||||||
wrappedDrawable
|
|
||||||
} else {
|
|
||||||
DrawableCompat.wrap(background)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMaterialShapeDrawableBackground(
|
|
||||||
@ColorInt backgroundColor: Int,
|
|
||||||
shapeAppearanceModel: ShapeAppearanceModel,
|
|
||||||
): MaterialShapeDrawable {
|
|
||||||
val background = MaterialShapeDrawable(shapeAppearanceModel)
|
|
||||||
background.fillColor = ColorStateList.valueOf(backgroundColor)
|
|
||||||
return background
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,10 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks
|
|
||||||
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
|
||||||
|
|
||||||
val bookmarksModule
|
|
||||||
get() = module {
|
|
||||||
|
|
||||||
factory { BookmarksRepository(get()) }
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.data
|
|
||||||
|
|
||||||
import androidx.room.Embedded
|
|
||||||
import androidx.room.Junction
|
|
||||||
import androidx.room.Relation
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
|
||||||
|
|
||||||
class BookmarkWithManga(
|
|
||||||
@Embedded val bookmark: BookmarkEntity,
|
|
||||||
@Relation(
|
|
||||||
parentColumn = "manga_id",
|
|
||||||
entityColumn = "manga_id"
|
|
||||||
)
|
|
||||||
val manga: MangaEntity,
|
|
||||||
@Relation(
|
|
||||||
parentColumn = "manga_id",
|
|
||||||
entityColumn = "tag_id",
|
|
||||||
associateBy = Junction(MangaTagsEntity::class)
|
|
||||||
)
|
|
||||||
val tags: List<TagEntity>,
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.data
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.Query
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
abstract class BookmarksDao {
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
|
||||||
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
|
|
||||||
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
|
||||||
|
|
||||||
@Insert
|
|
||||||
abstract suspend fun insert(entity: BookmarkEntity)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
|
||||||
abstract suspend fun delete(mangaId: Long, pageId: Long)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.domain
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Bookmark(
|
|
||||||
val manga: Manga,
|
|
||||||
val pageId: Long,
|
|
||||||
val chapterId: Long,
|
|
||||||
val page: Int,
|
|
||||||
val scroll: Int,
|
|
||||||
val imageUrl: String,
|
|
||||||
val createdAt: Date,
|
|
||||||
val percent: Float,
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Bookmark
|
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (pageId != other.pageId) return false
|
|
||||||
if (chapterId != other.chapterId) return false
|
|
||||||
if (page != other.page) return false
|
|
||||||
if (scroll != other.scroll) return false
|
|
||||||
if (imageUrl != other.imageUrl) return false
|
|
||||||
if (createdAt != other.createdAt) return false
|
|
||||||
if (percent != other.percent) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = manga.hashCode()
|
|
||||||
result = 31 * result + pageId.hashCode()
|
|
||||||
result = 31 * result + chapterId.hashCode()
|
|
||||||
result = 31 * result + page
|
|
||||||
result = 31 * result + scroll
|
|
||||||
result = 31 * result + imageUrl.hashCode()
|
|
||||||
result = 31 * result + createdAt.hashCode()
|
|
||||||
result = 31 * result + percent.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.domain
|
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.koitharu.kotatsu.bookmarks.data.toBookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.data.toEntity
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
|
||||||
|
|
||||||
class BookmarksRepository(
|
|
||||||
private val db: MangaDatabase,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> {
|
|
||||||
return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> {
|
|
||||||
return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addBookmark(bookmark: Bookmark) {
|
|
||||||
db.withTransaction {
|
|
||||||
val tags = bookmark.manga.tags.toEntities()
|
|
||||||
db.tagsDao.upsert(tags)
|
|
||||||
db.mangaDao.upsert(bookmark.manga.toEntity(), tags)
|
|
||||||
db.bookmarksDao.insert(bookmark.toEntity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
|
|
||||||
db.bookmarksDao.delete(mangaId, pageId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
|
|
||||||
class BookmarksAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
clickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<Bookmark>(
|
|
||||||
DiffCallback(),
|
|
||||||
bookmarkListAD(coil, lifecycleOwner, clickListener)
|
|
||||||
) {
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
|
||||||
return oldItem.manga.id == newItem.manga.id && oldItem.chapterId == newItem.chapterId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
|
||||||
return oldItem.imageUrl == newItem.imageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
|
||||||
|
|
||||||
interface CloudFlareCallback {
|
|
||||||
|
|
||||||
fun onPageLoaded()
|
|
||||||
|
|
||||||
fun onCheckPassed()
|
|
||||||
}
|
|
||||||
@@ -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,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(androidContext()) }
|
|
||||||
}
|
|
||||||
@@ -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,7 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
|
||||||
val url: String
|
|
||||||
) : IOException("Protected by CloudFlare")
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
class WrongPasswordException : SecurityException()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.github
|
|
||||||
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val githubModule
|
|
||||||
get() = module {
|
|
||||||
factory { 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/KotatsuApp/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,16 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import java.util.*
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class FavouriteCategory(
|
|
||||||
val id: Long,
|
|
||||||
val title: String,
|
|
||||||
val sortKey: Int,
|
|
||||||
val order: SortOrder,
|
|
||||||
val createdAt: Date,
|
|
||||||
val isTrackingEnabled: Boolean,
|
|
||||||
) : Parcelable
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
|
||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
||||||
import org.koitharu.kotatsu.utils.ext.iterator
|
|
||||||
|
|
||||||
fun Collection<Manga>.ids() = mapToSet { it.id }
|
|
||||||
|
|
||||||
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
|
||||||
val ch = chapters
|
|
||||||
if (ch.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (history != null) {
|
|
||||||
val currentChapter = ch.find { it.id == history.chapterId }
|
|
||||||
if (currentChapter != null) {
|
|
||||||
return currentChapter.branch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val groups = ch.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
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import androidx.core.os.ParcelCompat
|
|
||||||
import org.koitharu.kotatsu.parsers.model.*
|
|
||||||
|
|
||||||
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(title)
|
|
||||||
out.writeString(altTitle)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(publicUrl)
|
|
||||||
out.writeFloat(rating)
|
|
||||||
ParcelCompat.writeBoolean(out, isNsfw)
|
|
||||||
out.writeString(coverUrl)
|
|
||||||
out.writeString(largeCoverUrl)
|
|
||||||
out.writeString(description)
|
|
||||||
out.writeParcelable(ParcelableMangaTags(tags), flags)
|
|
||||||
out.writeSerializable(state)
|
|
||||||
out.writeString(author)
|
|
||||||
if (withChapters) {
|
|
||||||
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
|
|
||||||
} else {
|
|
||||||
out.writeString(null)
|
|
||||||
}
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readManga() = Manga(
|
|
||||||
id = readLong(),
|
|
||||||
title = requireNotNull(readString()),
|
|
||||||
altTitle = readString(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
publicUrl = requireNotNull(readString()),
|
|
||||||
rating = readFloat(),
|
|
||||||
isNsfw = ParcelCompat.readBoolean(this),
|
|
||||||
coverUrl = requireNotNull(readString()),
|
|
||||||
largeCoverUrl = readString(),
|
|
||||||
description = readString(),
|
|
||||||
tags = requireNotNull(readParcelable<ParcelableMangaTags>(ParcelableMangaTags::class.java.classLoader)).tags,
|
|
||||||
state = readSerializable() as MangaState?,
|
|
||||||
author = readString(),
|
|
||||||
chapters = readParcelable<ParcelableMangaChapters>(ParcelableMangaChapters::class.java.classLoader)?.chapters,
|
|
||||||
source = readSerializable() as MangaSource,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaPage.writeToParcel(out: Parcel) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(referer)
|
|
||||||
out.writeString(preview)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaPage() = MangaPage(
|
|
||||||
id = readLong(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
referer = requireNotNull(readString()),
|
|
||||||
preview = readString(),
|
|
||||||
source = readSerializable() as MangaSource,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaChapter.writeToParcel(out: Parcel) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(name)
|
|
||||||
out.writeInt(number)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(scanlator)
|
|
||||||
out.writeLong(uploadDate)
|
|
||||||
out.writeString(branch)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaChapter() = MangaChapter(
|
|
||||||
id = readLong(),
|
|
||||||
name = requireNotNull(readString()),
|
|
||||||
number = readInt(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
scanlator = readString(),
|
|
||||||
uploadDate = readLong(),
|
|
||||||
branch = readString(),
|
|
||||||
source = readSerializable() as MangaSource,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaTag.writeToParcel(out: Parcel) {
|
|
||||||
out.writeString(title)
|
|
||||||
out.writeString(key)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaTag() = MangaTag(
|
|
||||||
title = requireNotNull(readString()),
|
|
||||||
key = requireNotNull(readString()),
|
|
||||||
source = readSerializable() as MangaSource,
|
|
||||||
)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
// Limits to avoid TransactionTooLargeException
|
|
||||||
private const val MAX_SAFE_SIZE = 1024 * 512 // Assume that 512 kb is safe parcel size
|
|
||||||
private const val MAX_SAFE_CHAPTERS_COUNT = 40 // this is 100% safe
|
|
||||||
|
|
||||||
class ParcelableManga(
|
|
||||||
val manga: Manga,
|
|
||||||
private val withChapters: Boolean,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(parcel.readManga(), true)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
val chapters = manga.chapters
|
|
||||||
if (!withChapters || chapters == null) {
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
|
|
||||||
// fast path
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val tempParcel = Parcel.obtain()
|
|
||||||
manga.writeToParcel(tempParcel, flags, withChapters = true)
|
|
||||||
val size = tempParcel.dataSize()
|
|
||||||
if (size < MAX_SAFE_SIZE) {
|
|
||||||
parcel.appendFrom(tempParcel, 0, size)
|
|
||||||
} else {
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
|
||||||
}
|
|
||||||
tempParcel.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableManga {
|
|
||||||
return ParcelableManga(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableManga?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
|
|
||||||
class ParcelableMangaChapters(
|
|
||||||
val chapters: List<MangaChapter>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
List(parcel.readInt()) { parcel.readMangaChapter() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(chapters.size)
|
|
||||||
for (chapter in chapters) {
|
|
||||||
chapter.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
|
|
||||||
return ParcelableMangaChapters(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaChapters?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
|
|
||||||
class ParcelableMangaPages(
|
|
||||||
val pages: List<MangaPage>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
List(parcel.readInt()) { parcel.readMangaPage() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(pages.size)
|
|
||||||
for (page in pages) {
|
|
||||||
page.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
|
|
||||||
return ParcelableMangaPages(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaPages?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.utils.ext.Set
|
|
||||||
|
|
||||||
class ParcelableMangaTags(
|
|
||||||
val tags: Set<MangaTag>,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
|
||||||
Set(parcel.readInt()) { parcel.readMangaTag() }
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeInt(tags.size)
|
|
||||||
for (tag in tags) {
|
|
||||||
tag.writeToParcel(parcel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
|
|
||||||
return ParcelableMangaTags(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableMangaTags?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
class AndroidCookieJar : CookieJar {
|
|
||||||
|
|
||||||
private val cookieManager = CookieManager.getInstance()
|
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
|
||||||
return rawCookie.split(';').mapNotNull {
|
|
||||||
Cookie.parse(url, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
if (cookies.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val urlString = url.toString()
|
|
||||||
for (cookie in cookies) {
|
|
||||||
cookieManager.setCookie(urlString, cookie.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
|
||||||
cookieManager.removeAllCookies(continuation::resume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
|
||||||
|
|
||||||
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
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
val networkModule
|
|
||||||
get() = module {
|
|
||||||
single { AndroidCookieJar() } bind CookieJar::class
|
|
||||||
single {
|
|
||||||
val cache = get<LocalStorageManager>().createHttpCache()
|
|
||||||
OkHttpClient.Builder().apply {
|
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
|
||||||
cookieJar(get())
|
|
||||||
dns(DoHManager(cache, get()))
|
|
||||||
cache(cache)
|
|
||||||
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,105 +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 android.util.Size
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.room.InvalidationTracker
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
|
||||||
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.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
|
||||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
|
||||||
|
|
||||||
class ShortcutsUpdater(
|
|
||||||
private val context: Context,
|
|
||||||
private val coil: ImageLoader,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val mangaRepository: MangaDataRepository,
|
|
||||||
) : InvalidationTracker.Observer(TABLE_HISTORY) {
|
|
||||||
|
|
||||||
private val iconSize by lazy { getIconSize(context) }
|
|
||||||
private var shortcutsUpdateJob: Job? = null
|
|
||||||
|
|
||||||
override fun onInvalidated(tables: MutableSet<String>) {
|
|
||||||
val prevJob = shortcutsUpdateJob
|
|
||||||
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
prevJob?.join()
|
|
||||||
updateShortcutsImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun requestPinShortcut(manga: Manga): Boolean {
|
|
||||||
return ShortcutManagerCompat.requestPinShortcut(
|
|
||||||
context,
|
|
||||||
buildShortcutInfo(manga).build(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
suspend fun await(): Boolean {
|
|
||||||
return shortcutsUpdateJob?.join() != null
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateShortcutsImpl() = runCatching {
|
|
||||||
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
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
|
|
||||||
val icon = runCatching {
|
|
||||||
val bmp = coil.execute(
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(manga.coverUrl)
|
|
||||||
.size(iconSize.width, iconSize.height)
|
|
||||||
.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): Size {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
|
||||||
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
|
|
||||||
Size(it.iconMaxWidth, it.iconMaxHeight)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
|
|
||||||
Size(it, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import coil.map.Mapper
|
|
||||||
import coil.request.Options
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
|
||||||
|
|
||||||
class FaviconMapper : Mapper<Uri, HttpUrl> {
|
|
||||||
|
|
||||||
override fun map(data: Uri, options: Options): HttpUrl? {
|
|
||||||
if (data.scheme != "favicon") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null
|
|
||||||
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
|
|
||||||
return repo.getFaviconUrl().toHttpUrl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +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): List<Manga>
|
|
||||||
|
|
||||||
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): 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,48 +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): List<Manga> {
|
|
||||||
return parser.getList(offset, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
|
|
||||||
return parser.getList(offset, 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,327 +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.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 java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
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.core.network.DoHProvider
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
|
||||||
import org.koitharu.kotatsu.utils.ext.observe
|
|
||||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
|
||||||
|
|
||||||
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.GRID)
|
|
||||||
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() = DynamicColors.isDynamicColorAvailable() && 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 isTrackerEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
|
|
||||||
|
|
||||||
val isTrackerNotificationsEnabled: 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 defaultReaderMode: ReaderMode
|
|
||||||
get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
|
|
||||||
|
|
||||||
val isReaderModeDetectionEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
|
|
||||||
|
|
||||||
var isHistoryGroupingEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
|
|
||||||
|
|
||||||
val isReadingIndicatorsEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
|
|
||||||
|
|
||||||
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 isBiometricProtectionEnabled: Boolean
|
|
||||||
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
|
|
||||||
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
|
|
||||||
|
|
||||||
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 }).distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) }
|
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
|
||||||
|
|
||||||
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() = prefs.observe()
|
|
||||||
|
|
||||||
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_TRACKER_ENABLED = "tracker_enabled"
|
|
||||||
const val KEY_TRACK_SOURCES = "track_sources"
|
|
||||||
const val KEY_TRACK_CATEGORIES = "track_categories"
|
|
||||||
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_NOTIFICATIONS_INFO = "tracker_notifications_info"
|
|
||||||
const val KEY_READER_ANIMATION = "reader_animation"
|
|
||||||
const val KEY_READER_MODE = "reader_mode"
|
|
||||||
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
|
|
||||||
const val KEY_APP_PASSWORD = "app_password"
|
|
||||||
const val KEY_PROTECT_APP = "protect_app"
|
|
||||||
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
|
|
||||||
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_READING_INDICATORS = "reading_indicators"
|
|
||||||
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_SHIKIMORI = "shikimori"
|
|
||||||
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
|
|
||||||
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
|
|
||||||
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
|
|
||||||
const val KEY_DOH = "doh"
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
|
|
||||||
private const val NETWORK_NEVER = 0
|
|
||||||
private const val NETWORK_ALWAYS = 1
|
|
||||||
private const val NETWORK_NON_METERED = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
|
|
||||||
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
|
||||||
var lastValue: T = valueProducer()
|
|
||||||
emit(lastValue)
|
|
||||||
observe().collect {
|
|
||||||
if (it == key) {
|
|
||||||
val value = valueProducer()
|
|
||||||
if (value != lastValue) {
|
|
||||||
emit(value)
|
|
||||||
}
|
|
||||||
lastValue = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> AppSettings.observeAsLiveData(
|
|
||||||
context: CoroutineContext,
|
|
||||||
key: String,
|
|
||||||
valueProducer: AppSettings.() -> T
|
|
||||||
) = liveData(context) {
|
|
||||||
emit(valueProducer())
|
|
||||||
observe().collect {
|
|
||||||
if (it == key) {
|
|
||||||
val value = valueProducer()
|
|
||||||
if (value != latestValue) {
|
|
||||||
emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
private const val CATEGORY_ID = "cat_id"
|
|
||||||
|
|
||||||
class AppWidgetConfig(context: Context, val widgetId: Int) {
|
|
||||||
|
|
||||||
private val prefs = context.getSharedPreferences("appwidget_$widgetId", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
var categoryId: Long
|
|
||||||
get() = prefs.getLong(CATEGORY_ID, 0L)
|
|
||||||
set(value) = prefs.edit { putLong(CATEGORY_ID, value) }
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui
|
|
||||||
|
|
||||||
import android.text.Html
|
|
||||||
import coil.ComponentRegistry
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.disk.DiskCache
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
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.CacheDir
|
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
|
||||||
|
|
||||||
val uiModule
|
|
||||||
get() = module {
|
|
||||||
single {
|
|
||||||
val httpClientFactory = {
|
|
||||||
get<OkHttpClient>().newBuilder()
|
|
||||||
.cache(null)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
val diskCacheFactory = {
|
|
||||||
val context = androidContext()
|
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
|
||||||
DiskCache.Builder()
|
|
||||||
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
ImageLoader.Builder(androidContext())
|
|
||||||
.okHttpClient(httpClientFactory)
|
|
||||||
.interceptorDispatcher(Dispatchers.Default)
|
|
||||||
.fetcherDispatcher(Dispatchers.IO)
|
|
||||||
.decoderDispatcher(Dispatchers.Default)
|
|
||||||
.transformationDispatcher(Dispatchers.Default)
|
|
||||||
.diskCache(diskCacheFactory)
|
|
||||||
.components(
|
|
||||||
ComponentRegistry.Builder()
|
|
||||||
.add(CbzFetcher.Factory())
|
|
||||||
.add(FaviconMapper())
|
|
||||||
.build()
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
factory<Html.ImageGetter> { CoilImageGetter(androidContext(), get()) }
|
|
||||||
}
|
|
||||||
@@ -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(), get(), get(), get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.domain
|
|
||||||
|
|
||||||
class BranchComparator : Comparator<String?> {
|
|
||||||
|
|
||||||
override fun compare(o1: String?, o2: String?): Int = compareValues(o1, o2)
|
|
||||||
}
|
|
||||||
@@ -1,286 +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.view.ActionMode
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
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.ListSelectionController
|
|
||||||
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 org.koitharu.kotatsu.utils.ext.addMenuProvider
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class ChaptersFragment :
|
|
||||||
BaseFragment<FragmentChaptersBinding>(),
|
|
||||||
OnListItemClickListener<ChapterListItem>,
|
|
||||||
AdapterView.OnItemSelectedListener,
|
|
||||||
MenuItem.OnActionExpandListener,
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
ListSelectionController.Callback {
|
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
|
||||||
|
|
||||||
private var chaptersAdapter: ChaptersAdapter? = null
|
|
||||||
private var selectionController: ListSelectionController? = null
|
|
||||||
|
|
||||||
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)
|
|
||||||
selectionController = ListSelectionController(
|
|
||||||
activity = requireActivity(),
|
|
||||||
decoration = ChaptersSelectionDecoration(view.context),
|
|
||||||
registryOwner = this,
|
|
||||||
callback = this,
|
|
||||||
)
|
|
||||||
with(binding.recyclerViewChapters) {
|
|
||||||
checkNotNull(selectionController).attachToRecyclerView(this)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
addMenuProvider(ChaptersMenuProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
chaptersAdapter = null
|
|
||||||
selectionController = null
|
|
||||||
binding.spinnerBranches?.adapter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: ChapterListItem, view: View) {
|
|
||||||
if (selectionController?.onItemClick(item.chapter.id) == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.hasFlag(ChapterListItem.FLAG_MISSING)) {
|
|
||||||
(activity as? DetailsActivity)?.showChapterMissingDialog(item.chapter.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
|
|
||||||
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 {
|
|
||||||
return selectionController?.onItemLongClick(item.chapter.id) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
selectionController?.snapshot(),
|
|
||||||
)
|
|
||||||
mode.finish()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_delete -> {
|
|
||||||
val ids = selectionController?.peekCheckedIds()
|
|
||||||
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_range -> {
|
|
||||||
val controller = selectionController ?: return false
|
|
||||||
val items = chaptersAdapter?.items ?: return false
|
|
||||||
val ids = HashSet(controller.peekCheckedIds())
|
|
||||||
val buffer = HashSet<Long>()
|
|
||||||
var isAdding = false
|
|
||||||
for (x in items) {
|
|
||||||
if (x.chapter.id in ids) {
|
|
||||||
isAdding = true
|
|
||||||
if (buffer.isNotEmpty()) {
|
|
||||||
ids.addAll(buffer)
|
|
||||||
buffer.clear()
|
|
||||||
}
|
|
||||||
} else if (isAdding) {
|
|
||||||
buffer.add(x.chapter.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.addAll(ids)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_select_all -> {
|
|
||||||
val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false
|
|
||||||
selectionController?.addAll(ids)
|
|
||||||
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 = selectionController?.peekCheckedIds() ?: return false
|
|
||||||
val allItems = chaptersAdapter?.items.orEmpty()
|
|
||||||
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
|
|
||||||
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
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size
|
|
||||||
mode.title = items.size.toString()
|
|
||||||
var hasGap = false
|
|
||||||
for (i in 0 until items.size - 1) {
|
|
||||||
if (items[i].index + 1 != items[i + 1].index) {
|
|
||||||
hasGap = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_select_range).isVisible = hasGap
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSelectionChanged(count: Int) {
|
|
||||||
binding.recyclerViewChapters.invalidateItemDecorations()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ChaptersMenuProvider : MenuProvider {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.opt_chapters, menu)
|
|
||||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
|
||||||
searchMenuItem.setOnActionExpandListener(this@ChaptersFragment)
|
|
||||||
val searchView = searchMenuItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this@ChaptersFragment)
|
|
||||||
searchView.setIconifiedByDefault(false)
|
|
||||||
searchView.queryHint = searchMenuItem.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareMenu(menu: 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 onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
|
||||||
R.id.action_reversed -> {
|
|
||||||
viewModel.setChaptersReversed(!menuItem.isChecked)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,375 +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.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.snackbar.Snackbar
|
|
||||||
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.ShortcutsUpdater
|
|
||||||
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.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
|
|
||||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.isReportable
|
|
||||||
import org.koitharu.kotatsu.utils.ext.report
|
|
||||||
|
|
||||||
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)
|
|
||||||
viewModel.onShowToast.observe(this) {
|
|
||||||
binding.snackbar.show(messageText = getString(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
e.isReportable() -> {
|
|
||||||
binding.snackbar.show(
|
|
||||||
messageText = e.getDisplayMessage(resources),
|
|
||||||
actionId = R.string.report,
|
|
||||||
duration = if (viewModel.manga.value?.chapters == null) {
|
|
||||||
Snackbar.LENGTH_INDEFINITE
|
|
||||||
} else {
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
},
|
|
||||||
onActionClick = {
|
|
||||||
e.report("DetailsActivity::onError")
|
|
||||||
dismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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.maxCharacterCount = 3
|
|
||||||
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)
|
|
||||||
menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isScrobblingAvailable
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
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(MultiSearchActivity.newIntent(this, it.title))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_shiki_track -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
ScrobblingSelectorBottomSheet.show(supportFragmentManager, it)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.action_shortcut -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (!get<ShortcutsUpdater>().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,398 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.os.Bundle
|
|
||||||
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.toFile
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.view.MenuProvider
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
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.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksAdapter
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet
|
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
|
||||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
|
||||||
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.scrobbling.domain.model.ScrobblingInfo
|
|
||||||
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.ShareHelper
|
|
||||||
import org.koitharu.kotatsu.utils.ext.*
|
|
||||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
|
||||||
|
|
||||||
class DetailsFragment :
|
|
||||||
BaseFragment<FragmentDetailsBinding>(),
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
ChipsView.OnChipClickListener,
|
|
||||||
OnListItemClickListener<Bookmark> {
|
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
|
||||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
|
||||||
|
|
||||||
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.scrobblingLayout.root.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)
|
|
||||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
|
||||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
|
||||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
|
||||||
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
|
|
||||||
addMenuProvider(DetailsMenuProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: Bookmark, view: View) {
|
|
||||||
val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
|
|
||||||
startActivity(ReaderActivity.newIntent(view.context, item), options.toBundle())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
|
||||||
val menu = PopupMenu(view.context, view)
|
|
||||||
menu.inflate(R.menu.popup_bookmark)
|
|
||||||
menu.setOnMenuItemClickListener { menuItem ->
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.action_remove -> viewModel.removeBookmark(item)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
menu.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMangaUpdated(manga: Manga) {
|
|
||||||
with(binding) {
|
|
||||||
// Main
|
|
||||||
loadCover(manga)
|
|
||||||
textViewTitle.text = manga.title
|
|
||||||
textViewSubtitle.textAndVisible = manga.altTitle
|
|
||||||
textViewAuthor.textAndVisible = manga.author
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// Chips
|
|
||||||
bindTags(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
|
|
||||||
val infoLayout = binding.infoLayout
|
|
||||||
if (chapters.isNullOrEmpty()) {
|
|
||||||
infoLayout.textViewChapters.isVisible = false
|
|
||||||
} else {
|
|
||||||
infoLayout.textViewChapters.isVisible = true
|
|
||||||
infoLayout.textViewChapters.text = resources.getQuantityString(
|
|
||||||
R.plurals.chapters,
|
|
||||||
chapters.size,
|
|
||||||
chapters.size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Buttons
|
|
||||||
binding.buttonRead.isEnabled = !chapters.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDescriptionChanged(description: CharSequence?) {
|
|
||||||
if (description.isNullOrBlank()) {
|
|
||||||
binding.textViewDescription.setText(R.string.no_description)
|
|
||||||
} else {
|
|
||||||
binding.textViewDescription.text = description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onBookmarksChanged(bookmarks: List<Bookmark>) {
|
|
||||||
var adapter = binding.recyclerViewBookmarks.adapter as? BookmarksAdapter
|
|
||||||
binding.groupBookmarks.isGone = bookmarks.isEmpty()
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.items = bookmarks
|
|
||||||
} else {
|
|
||||||
adapter = BookmarksAdapter(coil, viewLifecycleOwner, this)
|
|
||||||
adapter.items = bookmarks
|
|
||||||
binding.recyclerViewBookmarks.adapter = adapter
|
|
||||||
val spacing = resources.getDimensionPixelOffset(R.dimen.bookmark_list_spacing)
|
|
||||||
binding.recyclerViewBookmarks.addItemDecoration(SpacingItemDecoration(spacing))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onScrobblingInfoChanged(scrobbling: ScrobblingInfo?) {
|
|
||||||
with(binding.scrobblingLayout) {
|
|
||||||
root.isVisible = scrobbling != null
|
|
||||||
if (scrobbling == null) {
|
|
||||||
CoilUtils.dispose(imageViewCover)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
imageViewCover.newImageRequest(scrobbling.coverUrl)?.run {
|
|
||||||
placeholder(R.drawable.ic_placeholder)
|
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_placeholder)
|
|
||||||
lifecycle(viewLifecycleOwner)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
textViewTitle.text = scrobbling.title
|
|
||||||
textViewTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, scrobbling.scrobbler.iconResId, 0)
|
|
||||||
ratingBar.rating = scrobbling.rating * ratingBar.numStars
|
|
||||||
textViewStatus.text = scrobbling.status?.let {
|
|
||||||
resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
val manga = viewModel.manga.value ?: return
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_favorite -> {
|
|
||||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, manga)
|
|
||||||
}
|
|
||||||
R.id.scrobbling_layout -> {
|
|
||||||
ScrobblingInfoBottomSheet.show(childFragmentManager)
|
|
||||||
}
|
|
||||||
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.makeScaleUpAnimation(v, 0, 0, v.width, v.height)
|
|
||||||
startActivity(
|
|
||||||
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { 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 imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
|
|
||||||
val lastResult = CoilUtils.result(binding.imageViewCover)
|
|
||||||
if (lastResult?.request?.data == imageUrl) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val request = ImageRequest.Builder(context ?: return)
|
|
||||||
.target(binding.imageViewCover)
|
|
||||||
.size(CoverSizeResolver(binding.imageViewCover))
|
|
||||||
.data(imageUrl)
|
|
||||||
.crossfade(true)
|
|
||||||
.referer(manga.publicUrl)
|
|
||||||
.lifecycle(viewLifecycleOwner)
|
|
||||||
.placeholderMemoryCacheKey(manga.coverUrl)
|
|
||||||
val previousDrawable = lastResult?.drawable
|
|
||||||
if (previousDrawable != null) {
|
|
||||||
request.fallback(previousDrawable)
|
|
||||||
.placeholder(previousDrawable)
|
|
||||||
.error(previousDrawable)
|
|
||||||
} else {
|
|
||||||
request.fallback(R.drawable.ic_placeholder)
|
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
|
||||||
.error(R.drawable.ic_placeholder)
|
|
||||||
}
|
|
||||||
request.enqueueWith(coil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class DetailsMenuProvider : MenuProvider {
|
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
menuInflater.inflate(R.menu.opt_details_info, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
|
||||||
R.id.action_share -> {
|
|
||||||
viewModel.manga.value?.let {
|
|
||||||
val context = requireContext()
|
|
||||||
if (it.source == MangaSource.LOCAL) {
|
|
||||||
ShareHelper(context).shareCbz(listOf(it.url.toUri().toFile()))
|
|
||||||
} else {
|
|
||||||
ShareHelper(context).shareMangaLink(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import android.text.Html
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.ForegroundColorSpan
|
|
||||||
import androidx.core.text.getSpans
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asFlow
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
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.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.details.domain.BranchComparator
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
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.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
|
||||||
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.printStackTraceDebug
|
|
||||||
|
|
||||||
class DetailsViewModel(
|
|
||||||
intent: MangaIntent,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
favouritesRepository: FavouritesRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
trackingRepository: TrackingRepository,
|
|
||||||
mangaDataRepository: MangaDataRepository,
|
|
||||||
private val bookmarksRepository: BookmarksRepository,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val scrobbler: Scrobbler,
|
|
||||||
private val imageGetter: Html.ImageGetter,
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
private val delegate = MangaDetailsDelegate(
|
|
||||||
intent = intent,
|
|
||||||
settings = settings,
|
|
||||||
mangaDataRepository = mangaDataRepository,
|
|
||||||
historyRepository = historyRepository,
|
|
||||||
localMangaRepository = localMangaRepository,
|
|
||||||
)
|
|
||||||
|
|
||||||
private var loadingJob: Job
|
|
||||||
|
|
||||||
val onShowToast = SingleLiveEvent<Int>()
|
|
||||||
|
|
||||||
private val history = historyRepository.observeOne(delegate.mangaId)
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
||||||
|
|
||||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val newChapters = trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
|
||||||
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
|
||||||
|
|
||||||
private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
val manga = delegate.manga.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 bookmarks = delegate.manga.flatMapLatest {
|
|
||||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
|
||||||
|
|
||||||
val description = delegate.manga
|
|
||||||
.distinctUntilChangedBy { it?.description.orEmpty() }
|
|
||||||
.transformLatest {
|
|
||||||
val description = it?.description
|
|
||||||
if (description.isNullOrEmpty()) {
|
|
||||||
emit(null)
|
|
||||||
} else {
|
|
||||||
emit(description.parseAsHtml().filterSpans())
|
|
||||||
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
|
|
||||||
}
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
|
||||||
|
|
||||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
|
||||||
val isScrobblingAvailable: Boolean
|
|
||||||
get() = scrobbler.isAvailable
|
|
||||||
|
|
||||||
val scrobblingInfo = scrobbler.observeScrobblingInfo(delegate.mangaId)
|
|
||||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
|
||||||
|
|
||||||
val branches: LiveData<List<String?>> = delegate.manga.map {
|
|
||||||
val chapters = it?.chapters ?: return@map emptyList()
|
|
||||||
chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val selectedBranchIndex = combine(
|
|
||||||
branches.asFlow(),
|
|
||||||
delegate.selectedBranch,
|
|
||||||
) { branches, selected ->
|
|
||||||
branches.indexOf(selected)
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val isChaptersEmpty: LiveData<Boolean> = combine(
|
|
||||||
delegate.manga,
|
|
||||||
isLoading.asFlow(),
|
|
||||||
) { m, loading ->
|
|
||||||
m != null && m.chapters.isNullOrEmpty() && !loading
|
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext, false)
|
|
||||||
|
|
||||||
val chapters = combine(
|
|
||||||
combine(
|
|
||||||
delegate.manga,
|
|
||||||
delegate.relatedManga,
|
|
||||||
history,
|
|
||||||
delegate.selectedBranch,
|
|
||||||
newChapters,
|
|
||||||
) { manga, related, history, branch, news ->
|
|
||||||
delegate.mapChapters(manga, related, history, news, branch)
|
|
||||||
},
|
|
||||||
chaptersReversed,
|
|
||||||
chaptersQuery,
|
|
||||||
) { list, reversed, query ->
|
|
||||||
(if (reversed) list.asReversed() else list).filterSearch(query)
|
|
||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
|
||||||
|
|
||||||
val selectedBranchValue: String?
|
|
||||||
get() = delegate.selectedBranch.value
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadingJob = doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reload() {
|
|
||||||
loadingJob.cancel()
|
|
||||||
loadingJob = doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteLocal() {
|
|
||||||
val m = delegate.manga.value
|
|
||||||
if (m == null) {
|
|
||||||
onShowToast.call(R.string.file_not_found)
|
|
||||||
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 removeBookmark(bookmark: Bookmark) {
|
|
||||||
launchJob {
|
|
||||||
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
|
|
||||||
onShowToast.call(R.string.bookmark_removed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChaptersReversed(newValue: Boolean) {
|
|
||||||
settings.chaptersReverse = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedBranch(branch: String?) {
|
|
||||||
delegate.selectedBranch.value = branch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRemoteManga(): Manga? {
|
|
||||||
return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performChapterSearch(query: String?) {
|
|
||||||
chaptersQuery.value = query?.trim().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDownloadComplete(downloadedManga: Manga) {
|
|
||||||
val currentManga = delegate.manga.value ?: return
|
|
||||||
if (currentManga.id != downloadedManga.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentManga.source == MangaSource.LOCAL) {
|
|
||||||
reload()
|
|
||||||
} else {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
|
||||||
runCatching {
|
|
||||||
localMangaRepository.getDetails(downloadedManga)
|
|
||||||
}.onSuccess {
|
|
||||||
delegate.relatedManga.value = it
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateScrobbling(rating: Float, status: ScrobblingStatus?) {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
scrobbler.updateScrobblingInfo(
|
|
||||||
mangaId = delegate.mangaId,
|
|
||||||
rating = rating,
|
|
||||||
status = status,
|
|
||||||
comment = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregisterScrobbling() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
scrobbler.unregisterScrobbling(
|
|
||||||
mangaId = delegate.mangaId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
|
||||||
delegate.doLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Spanned.filterSpans(): CharSequence {
|
|
||||||
val spannable = SpannableString.valueOf(this)
|
|
||||||
val spans = spannable.getSpans<ForegroundColorSpan>()
|
|
||||||
for (span in spans) {
|
|
||||||
spannable.removeSpan(span)
|
|
||||||
}
|
|
||||||
return spannable.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,164 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
|
||||||
import org.koitharu.kotatsu.base.domain.MangaIntent
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
|
||||||
import org.koitharu.kotatsu.core.model.getPreferredBranch
|
|
||||||
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.history.domain.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
|
||||||
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.utils.ext.printStackTraceDebug
|
|
||||||
|
|
||||||
class MangaDetailsDelegate(
|
|
||||||
private val intent: MangaIntent,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val mangaDataRepository: MangaDataRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val mangaData = MutableStateFlow(intent.manga)
|
|
||||||
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
// Remote manga for saved and saved for remote
|
|
||||||
val relatedManga = MutableStateFlow<Manga?>(null)
|
|
||||||
val manga: StateFlow<Manga?>
|
|
||||||
get() = mangaData
|
|
||||||
val mangaId = intent.manga?.id ?: intent.mangaId
|
|
||||||
|
|
||||||
suspend fun doLoad() {
|
|
||||||
var manga = mangaDataRepository.resolveIntent(intent) ?: throw NotFoundException("Cannot find manga", "")
|
|
||||||
mangaData.value = manga
|
|
||||||
manga = MangaRepository(manga.source).getDetails(manga)
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
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 ->
|
|
||||||
error.printStackTraceDebug()
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapChapters(
|
|
||||||
manga: Manga?,
|
|
||||||
related: Manga?,
|
|
||||||
history: MangaHistory?,
|
|
||||||
newCount: Int,
|
|
||||||
branch: String?,
|
|
||||||
): List<ChapterListItem> {
|
|
||||||
val chapters = manga?.chapters ?: return emptyList()
|
|
||||||
val relatedChapters = related?.chapters
|
|
||||||
return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) {
|
|
||||||
mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
} else {
|
|
||||||
mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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?.mapTo(HashSet(downloadedChapters.size)) { 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (result.size < chapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
if (result.size < sourceChapters.size / 2) {
|
|
||||||
result.trimToSize()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.adapter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class ChaptersAdapter(
|
|
||||||
onItemClickListener: OnListItemClickListener<ChapterListItem>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ChapterListItem>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
delegatesManager.addDelegate(chapterListItemAD(onItemClickListener))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return items[position].chapter.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
|
||||||
return oldItem.chapter.id == newItem.chapter.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: ChapterListItem,
|
|
||||||
newItem: ChapterListItem
|
|
||||||
): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ChapterListItem, newItem: ChapterListItem): Any? {
|
|
||||||
if (oldItem.flags != newItem.flags && oldItem.chapter == newItem.chapter) {
|
|
||||||
return newItem.flags
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.model
|
|
||||||
|
|
||||||
import java.text.DateFormat
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
|
|
||||||
class ChapterListItem(
|
|
||||||
val chapter: MangaChapter,
|
|
||||||
val flags: Int,
|
|
||||||
private val uploadDateMs: Long,
|
|
||||||
private val dateFormat: DateFormat,
|
|
||||||
) {
|
|
||||||
|
|
||||||
var uploadDate: String? = null
|
|
||||||
private set
|
|
||||||
get() {
|
|
||||||
if (field != null) return field
|
|
||||||
if (uploadDateMs == 0L) return null
|
|
||||||
field = dateFormat.format(uploadDateMs)
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
val status: Int
|
|
||||||
get() = flags and MASK_STATUS
|
|
||||||
|
|
||||||
fun hasFlag(flag: Int): Boolean {
|
|
||||||
return (flags and flag) == flag
|
|
||||||
}
|
|
||||||
|
|
||||||
fun description(): CharSequence? {
|
|
||||||
val scanlator = chapter.scanlator?.takeUnless { it.isBlank() }
|
|
||||||
return when {
|
|
||||||
uploadDate != null && scanlator != null -> "$uploadDate • $scanlator"
|
|
||||||
scanlator != null -> scanlator
|
|
||||||
else -> uploadDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ChapterListItem
|
|
||||||
|
|
||||||
if (chapter != other.chapter) return false
|
|
||||||
if (flags != other.flags) return false
|
|
||||||
if (uploadDateMs != other.uploadDateMs) return false
|
|
||||||
if (dateFormat != other.dateFormat) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = chapter.hashCode()
|
|
||||||
result = 31 * result + flags
|
|
||||||
result = 31 * result + uploadDateMs.hashCode()
|
|
||||||
result = 31 * result + dateFormat.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val FLAG_UNREAD = 2
|
|
||||||
const val FLAG_CURRENT = 4
|
|
||||||
const val FLAG_NEW = 8
|
|
||||||
const val FLAG_MISSING = 16
|
|
||||||
const val FLAG_DOWNLOADED = 32
|
|
||||||
const val MASK_STATUS = FLAG_UNREAD or FLAG_CURRENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.scrobbling
|
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.RatingBar
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
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.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetScrobblingBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
|
||||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
|
||||||
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|
||||||
|
|
||||||
class ScrobblingInfoBottomSheet :
|
|
||||||
BaseBottomSheet<SheetScrobblingBinding>(),
|
|
||||||
AdapterView.OnItemSelectedListener,
|
|
||||||
RatingBar.OnRatingBarChangeListener,
|
|
||||||
View.OnClickListener,
|
|
||||||
PopupMenu.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private val viewModel by sharedViewModel<DetailsViewModel>()
|
|
||||||
private val coil by inject<ImageLoader>(mode = LazyThreadSafetyMode.NONE)
|
|
||||||
private var menu: PopupMenu? = null
|
|
||||||
|
|
||||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetScrobblingBinding {
|
|
||||||
return SheetScrobblingBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
|
||||||
viewModel.onError.observe(viewLifecycleOwner) {
|
|
||||||
Toast.makeText(view.context, it.getDisplayMessage(view.resources), Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.spinnerStatus.onItemSelectedListener = this
|
|
||||||
binding.ratingBar.onRatingBarChangeListener = this
|
|
||||||
binding.buttonMenu.setOnClickListener(this)
|
|
||||||
binding.imageViewCover.setOnClickListener(this)
|
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
|
|
||||||
menu = PopupMenu(view.context, binding.buttonMenu).apply {
|
|
||||||
inflate(R.menu.opt_scrobbling)
|
|
||||||
setOnMenuItemClickListener(this@ScrobblingInfoBottomSheet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
menu = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
viewModel.updateScrobbling(
|
|
||||||
rating = binding.ratingBar.rating / binding.ratingBar.numStars,
|
|
||||||
status = enumValues<ScrobblingStatus>().getOrNull(position),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
|
|
||||||
override fun onRatingChanged(ratingBar: RatingBar, rating: Float, fromUser: Boolean) {
|
|
||||||
if (fromUser) {
|
|
||||||
viewModel.updateScrobbling(
|
|
||||||
rating = rating / ratingBar.numStars,
|
|
||||||
status = enumValues<ScrobblingStatus>().getOrNull(binding.spinnerStatus.selectedItemPosition),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_menu -> menu?.show()
|
|
||||||
R.id.imageView_cover -> {
|
|
||||||
val coverUrl = viewModel.scrobblingInfo.value?.coverUrl ?: return
|
|
||||||
val options = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.width, v.height)
|
|
||||||
startActivity(ImageActivity.newIntent(v.context, coverUrl), options.toBundle())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onScrobblingInfoChanged(scrobbling: ScrobblingInfo?) {
|
|
||||||
if (scrobbling == null) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
binding.textViewTitle.text = scrobbling.title
|
|
||||||
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
|
|
||||||
binding.textViewDescription.text = scrobbling.description
|
|
||||||
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
|
|
||||||
ImageRequest.Builder(context ?: return)
|
|
||||||
.target(binding.imageViewCover)
|
|
||||||
.data(scrobbling.coverUrl)
|
|
||||||
.crossfade(true)
|
|
||||||
.lifecycle(viewLifecycleOwner)
|
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
|
||||||
.fallback(R.drawable.ic_placeholder)
|
|
||||||
.error(R.drawable.ic_placeholder)
|
|
||||||
.scale(Scale.FILL)
|
|
||||||
.enqueueWith(coil)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "ScrobblingInfoBottomSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = ScrobblingInfoBottomSheet().show(fm, TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_browser -> {
|
|
||||||
val url = viewModel.scrobblingInfo.value?.externalUrl ?: return false
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
|
||||||
startActivity(
|
|
||||||
Intent.createChooser(intent, getString(R.string.open_in_browser))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.action_unregister -> {
|
|
||||||
viewModel.unregisterScrobbling()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
R.id.action_edit -> {
|
|
||||||
val manga = viewModel.manga.value ?: return false
|
|
||||||
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.domain
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import coil.size.Scale
|
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
|
||||||
import kotlinx.coroutines.sync.withPermit
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import okio.IOException
|
|
||||||
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.download.ui.service.PausingHandle
|
|
||||||
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.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.utils.ext.referer
|
|
||||||
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
|
||||||
|
|
||||||
private const val MAX_FAILSAFE_ATTEMPTS = 2
|
|
||||||
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 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,
|
|
||||||
): PausingProgressJob<DownloadState> {
|
|
||||||
val stateFlow = MutableStateFlow<DownloadState>(
|
|
||||||
DownloadState.Queued(startId = startId, manga = manga, cover = null),
|
|
||||||
)
|
|
||||||
val pausingHandle = PausingHandle()
|
|
||||||
val job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(stateFlow)) {
|
|
||||||
try {
|
|
||||||
downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
|
|
||||||
} catch (e: CancellationException) { // handle cancellation if not handled already
|
|
||||||
val state = stateFlow.value
|
|
||||||
if (state !is DownloadState.Cancelled) {
|
|
||||||
stateFlow.value = DownloadState.Cancelled(startId, state.manga, state.cover)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return PausingProgressJob(job, stateFlow, pausingHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun downloadMangaImpl(
|
|
||||||
manga: Manga,
|
|
||||||
chaptersIds: LongArray?,
|
|
||||||
outState: MutableStateFlow<DownloadState>,
|
|
||||||
pausingHandle: PausingHandle,
|
|
||||||
startId: Int,
|
|
||||||
) {
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
var manga = manga
|
|
||||||
val chaptersIdsSet = chaptersIds?.toMutableSet()
|
|
||||||
val cover = loadCover(manga)
|
|
||||||
outState.value = DownloadState.Queued(startId, manga, cover)
|
|
||||||
withMangaLock(manga) {
|
|
||||||
semaphore.withPermit {
|
|
||||||
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 = runFailsafe(outState, pausingHandle) {
|
|
||||||
repo.getPages(chapter)
|
|
||||||
}
|
|
||||||
for ((pageIndex, page) in pages.withIndex()) {
|
|
||||||
runFailsafe(outState, pausingHandle) {
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
outState.value = DownloadState.Progress(
|
|
||||||
startId = startId,
|
|
||||||
manga = data,
|
|
||||||
cover = 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) {
|
|
||||||
e.printStackTraceDebug()
|
|
||||||
outState.value = DownloadState.Error(startId, manga, cover, e, false)
|
|
||||||
} finally {
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
output?.closeQuietly()
|
|
||||||
output?.cleanup()
|
|
||||||
File(destination, tempFileName).deleteAwait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun <R> runFailsafe(
|
|
||||||
outState: MutableStateFlow<DownloadState>,
|
|
||||||
pausingHandle: PausingHandle,
|
|
||||||
block: suspend () -> R,
|
|
||||||
): R {
|
|
||||||
var countDown = MAX_FAILSAFE_ATTEMPTS
|
|
||||||
failsafe@ while (true) {
|
|
||||||
try {
|
|
||||||
return block()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
if (countDown <= 0) {
|
|
||||||
val state = outState.value
|
|
||||||
outState.value = DownloadState.Error(state.startId, state.manga, state.cover, e, true)
|
|
||||||
countDown = MAX_FAILSAFE_ATTEMPTS
|
|
||||||
pausingHandle.pause()
|
|
||||||
pausingHandle.awaitResumed()
|
|
||||||
outState.value = state
|
|
||||||
} else {
|
|
||||||
countDown--
|
|
||||||
delay(DOWNLOAD_ERROR_DELAY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ->
|
|
||||||
throwable.printStackTraceDebug()
|
|
||||||
val prevValue = outState.value
|
|
||||||
outState.value = DownloadState.Error(
|
|
||||||
startId = prevValue.startId,
|
|
||||||
manga = prevValue.manga,
|
|
||||||
cover = prevValue.cover,
|
|
||||||
error = throwable,
|
|
||||||
canRetry = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
private suspend inline fun <T> withMangaLock(manga: Manga, block: () -> T) = try {
|
|
||||||
localMangaRepository.lockManga(manga.id)
|
|
||||||
block()
|
|
||||||
} finally {
|
|
||||||
localMangaRepository.unlockManga(manga.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,227 +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 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,
|
|
||||||
val canRetry: Boolean,
|
|
||||||
) : 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
|
|
||||||
if (canRetry != other.canRetry) 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()
|
|
||||||
result = 31 * result + canRetry.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