Compare commits
594 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3acca44b5e | ||
|
|
c7da4feb8f | ||
|
|
baee9bee0e | ||
|
|
ec41d36508 | ||
|
|
8b63d227a7 | ||
|
|
c9b48c8207 | ||
|
|
6d7ce5205e | ||
|
|
5a02d534c9 | ||
|
|
6128e5b699 | ||
|
|
717a0ad4fb | ||
|
|
dee94ac0c4 | ||
|
|
9eec9a9957 | ||
|
|
a4966b4661 | ||
|
|
58e570601d | ||
|
|
7247cba855 | ||
|
|
d6012f9ddd | ||
|
|
2eedd0b4a8 | ||
|
|
5e6da9bb1c | ||
|
|
2f2a5b868d | ||
|
|
3f2e32dcc2 | ||
|
|
004109a6bc | ||
|
|
6159ee36c4 | ||
|
|
3b7d83dd6f | ||
|
|
877a018ced | ||
|
|
2e80b330e9 | ||
|
|
42ca38e693 | ||
|
|
d2fc3354af | ||
|
|
2a870e6167 | ||
|
|
393a9c2791 | ||
|
|
4c69839076 | ||
|
|
e37455e790 | ||
|
|
36259ba901 | ||
|
|
5b041b9a49 | ||
|
|
1734e888d6 | ||
|
|
9108646cea | ||
|
|
c6d1cf2f72 | ||
|
|
a317236cb0 | ||
|
|
3703c07a98 | ||
|
|
b7b32dd447 | ||
|
|
c103386dc5 | ||
|
|
a9d6ee4a95 | ||
|
|
71f2c91e5a | ||
|
|
b878f358ff | ||
|
|
4c9989da78 | ||
|
|
8e8424022a | ||
|
|
86504b8bde | ||
|
|
f082fa084f | ||
|
|
040fe258e9 | ||
|
|
1076009572 | ||
|
|
40dde71a1d | ||
|
|
9503aabf78 | ||
|
|
4ee16cfa2f | ||
|
|
f1c9eacaf0 | ||
|
|
920e16be10 | ||
|
|
4e0e5be726 | ||
|
|
f18eca52af | ||
|
|
451a155e08 | ||
|
|
0612a7ad2c | ||
|
|
9c34f25eda | ||
|
|
42360c678f | ||
|
|
04a3d02aa9 | ||
|
|
808fd13ad0 | ||
|
|
88c8dc4761 | ||
|
|
a7eba67a97 | ||
|
|
c27586231a | ||
|
|
db3db4637c | ||
|
|
bb2294f248 | ||
|
|
afd56c02e6 | ||
|
|
dcf1ffc976 | ||
|
|
91b7028b1a | ||
|
|
734c217c03 | ||
|
|
de18c6eb71 | ||
|
|
034be6b44e | ||
|
|
995ff5a764 | ||
|
|
102bec04d6 | ||
|
|
d05d807614 | ||
|
|
bffd75f4d9 | ||
|
|
bdaf3da7e0 | ||
|
|
353d856bf5 | ||
|
|
fca9ba98cd | ||
|
|
5df76fd881 | ||
|
|
54c646ceb0 | ||
|
|
3599f2f1b8 | ||
|
|
b2e53d4938 | ||
|
|
0d62408918 | ||
|
|
2ae046d4c5 | ||
|
|
66356dc094 | ||
|
|
ae16110a80 | ||
|
|
c3aff60a9c | ||
|
|
cfdca3434b | ||
|
|
b2c2693aba | ||
|
|
5901c26ae0 | ||
|
|
d864c73faf | ||
|
|
30551a56b2 | ||
|
|
23cb023a85 | ||
|
|
10291d5b29 | ||
|
|
e05e09f846 | ||
|
|
0ce7f7cf6b | ||
|
|
4d9d15004c | ||
|
|
1908ce3e46 | ||
|
|
6c07abec56 | ||
|
|
64dc646fc5 | ||
|
|
357669d8b2 | ||
|
|
21639ddcbc | ||
|
|
5183d5e882 | ||
|
|
3008b7b89a | ||
|
|
53e00e4689 | ||
|
|
963d7d8d42 | ||
|
|
1a7b1e7bdc | ||
|
|
b1fa9d1d22 | ||
|
|
91179ef901 | ||
|
|
a7a9ee9d59 | ||
|
|
ff05f3f79d | ||
|
|
c0062c83c8 | ||
|
|
ef0cf4766a | ||
|
|
910069ec99 | ||
|
|
d56107bf1f | ||
|
|
03426694c8 | ||
|
|
385003bcc8 | ||
|
|
225aacff43 | ||
|
|
208c0a494b | ||
|
|
0045c7cf44 | ||
|
|
eed7f89518 | ||
|
|
80c8b9eac0 | ||
|
|
53a680d13c | ||
|
|
3e77df20a2 | ||
|
|
7c1c0a38fa | ||
|
|
012eefe4fe | ||
|
|
cb0f0c70d0 | ||
|
|
23111dfef9 | ||
|
|
d050c9ad0e | ||
|
|
efd952a91a | ||
|
|
d3f23ea3a3 | ||
|
|
acba312e8d | ||
|
|
880dd6da27 | ||
|
|
0c839ce49a | ||
|
|
1afd2d3976 | ||
|
|
f2d881f9bc | ||
|
|
c838e57f22 | ||
|
|
2075b1be19 | ||
|
|
cf33cb66c6 | ||
|
|
8010c5079b | ||
|
|
a87a77083e | ||
|
|
ca20422344 | ||
|
|
c213b9d4b5 | ||
|
|
95fbe496cb | ||
|
|
b9fd2e100d | ||
|
|
1242a88f8e | ||
|
|
55851fb22f | ||
|
|
7801456d17 | ||
|
|
38a1fafa26 | ||
|
|
aa02233883 | ||
|
|
5405fdb85a | ||
|
|
38ad7e1fd4 | ||
|
|
06372083fd | ||
|
|
d5d3154074 | ||
|
|
1a279966d9 | ||
|
|
3222c2128e | ||
|
|
872c859efe | ||
|
|
b79c00f8df | ||
|
|
e7d3d9811d | ||
|
|
4fdfc75833 | ||
|
|
9754ebf1bb | ||
|
|
fee35cceab | ||
|
|
b928c4123c | ||
|
|
b093a885c9 | ||
|
|
dd898579c9 | ||
|
|
73143d2f94 | ||
|
|
563752f6a4 | ||
|
|
7135902100 | ||
|
|
969947ef71 | ||
|
|
806e4eade6 | ||
|
|
063cfbe6b9 | ||
|
|
7cb94a3baa | ||
|
|
894c584c78 | ||
|
|
2f65e7776a | ||
|
|
76c56c9119 | ||
|
|
e0a803399c | ||
|
|
7803f42486 | ||
|
|
39713b3cf6 | ||
|
|
8ebf5cea62 | ||
|
|
663dabe218 | ||
|
|
3a5d0120bf | ||
|
|
a773f932d4 | ||
|
|
2a5812735f | ||
|
|
06ec145802 | ||
|
|
6624778f7f | ||
|
|
1af1f071ad | ||
|
|
f87db4e6d3 | ||
|
|
07bd66fb39 | ||
|
|
4bb0d52217 | ||
|
|
66de4bd49e | ||
|
|
ff12d63696 | ||
|
|
c168a841f3 | ||
|
|
8bfb676e6a | ||
|
|
d5c0ce280e | ||
|
|
b34627c361 | ||
|
|
cbc3be056a | ||
|
|
d9acc4ec18 | ||
|
|
577cc848ee | ||
|
|
8a64c88a07 | ||
|
|
1cd7745e38 | ||
|
|
395b3f7200 | ||
|
|
b8db4c81d8 | ||
|
|
98bd42f3ae | ||
|
|
db8835a7b8 | ||
|
|
afe50a9ed6 | ||
|
|
beba818f57 | ||
|
|
beb17ef442 | ||
|
|
24f1546019 | ||
|
|
1b0fed5c56 | ||
|
|
3d32bd9d58 | ||
|
|
590120433c | ||
|
|
4bd7656681 | ||
|
|
2c7438e64d | ||
|
|
665bebaa7b | ||
|
|
6ed5994726 | ||
|
|
311ed865b7 | ||
|
|
b59fb678fe | ||
|
|
ed9ebdcc55 | ||
|
|
74569615e3 | ||
|
|
f3c320a90f | ||
|
|
a3012ab458 | ||
|
|
6ec58879fd | ||
|
|
571cf08c53 | ||
|
|
fca53eee7a | ||
|
|
ed9e2eb4d2 | ||
|
|
c0e94f8415 | ||
|
|
e172d619a1 | ||
|
|
d6c64fc638 | ||
|
|
37404cb9a6 | ||
|
|
9d5271ff26 | ||
|
|
5f59432e48 | ||
|
|
5c082b5cdb | ||
|
|
32133d3358 | ||
|
|
366e4f0da8 | ||
|
|
3ef033c700 | ||
|
|
bef8e4652f | ||
|
|
8bfdf07a2f | ||
|
|
f3e597275b | ||
|
|
11feaae216 | ||
|
|
fe2c1f9634 | ||
|
|
0c7c6dc48a | ||
|
|
503652f024 | ||
|
|
0c4adc67ea | ||
|
|
c7f5ce30b5 | ||
|
|
59d538824f | ||
|
|
de79f39d16 | ||
|
|
9792da3a5c | ||
|
|
c2407e6e41 | ||
|
|
7321eeaed9 | ||
|
|
9876adf676 | ||
|
|
d29e979fbf | ||
|
|
35baf4b58d | ||
|
|
97524d66f2 | ||
|
|
5b53f8c27d | ||
|
|
d4588570e6 | ||
|
|
cc2f9d4529 | ||
|
|
3def71ccc1 | ||
|
|
b313c64648 | ||
|
|
f7e7c84317 | ||
|
|
ee1c532d53 | ||
|
|
6993cec85e | ||
|
|
0b19f56215 | ||
|
|
817ce7e8df | ||
|
|
2b2498cb38 | ||
|
|
e4efd0f696 | ||
|
|
fbb267e11c | ||
|
|
5740af05fa | ||
|
|
ae2cc1dffc | ||
|
|
a5b9712e9f | ||
|
|
c013e6e4f4 | ||
|
|
0249faa3f6 | ||
|
|
9c52423dc0 | ||
|
|
1f7e5458ae | ||
|
|
b4d487b398 | ||
|
|
0281f1eadb | ||
|
|
1bd9b655f9 | ||
|
|
ed87292921 | ||
|
|
861be7614e | ||
|
|
717fe8748a | ||
|
|
c7a1312cd6 | ||
|
|
b2927854d4 | ||
|
|
cfda150630 | ||
|
|
4fa1382ce9 | ||
|
|
43075c52d1 | ||
|
|
87942747fc | ||
|
|
bb6cd73acd | ||
|
|
6790e5b0d4 | ||
|
|
845c356a73 | ||
|
|
34499ea77d | ||
|
|
6210864280 | ||
|
|
19084419c7 | ||
|
|
84ce4c508c | ||
|
|
0db8fafe61 | ||
|
|
fed241215e | ||
|
|
761f24daf9 | ||
|
|
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 | ||
|
|
f01fd18711 | ||
|
|
0098bdd07e |
@@ -13,6 +13,7 @@ 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
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
|||||||
ko_fi: xtimms
|
|
||||||
custom: ["https://yoomoney.ru/to/410012543938752"]
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,11 +10,13 @@
|
|||||||
/.idea/compiler.xml
|
/.idea/compiler.xml
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
|
/.idea/ktlint-plugin.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
/.idea/kotlinScripting.xml
|
/.idea/kotlinScripting.xml
|
||||||
/.idea/kotlinc.xml
|
/.idea/kotlinc.xml
|
||||||
/.idea/deploymentTargetDropDown.xml
|
/.idea/deploymentTargetDropDown.xml
|
||||||
/.idea/androidTestResultsUserPreferences.xml
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
|
/.idea/deploymentTargetSelector.xml
|
||||||
/.idea/render.experimental.xml
|
/.idea/render.experimental.xml
|
||||||
/.idea/inspectionProfiles/
|
/.idea/inspectionProfiles/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -5,7 +5,6 @@
|
|||||||
<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="jbr-17" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
@@ -14,6 +13,7 @@
|
|||||||
<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>
|
||||||
|
|||||||
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.
|
||||||
@@ -39,6 +39,10 @@ Kotatsu is a free and open source manga reader for Android.
|
|||||||
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)
|
||||||
|
|||||||
112
app/build.gradle
112
app/build.gradle
@@ -2,30 +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'
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 33
|
compileSdk = 34
|
||||||
buildToolsVersion = '33.0.2'
|
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
|
||||||
//TODO: update as soon as sources becomes available
|
targetSdk = 34
|
||||||
//noinspection OldTargetApi
|
versionCode = 611
|
||||||
targetSdkVersion 33
|
versionName = '6.6'
|
||||||
versionCode 560
|
|
||||||
versionName '5.3.3'
|
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
|
ksp {
|
||||||
kapt {
|
arg('room.generateKotlin', 'true')
|
||||||
arguments {
|
arg('room.schemaLocation', "$projectDir/schemas")
|
||||||
arg 'room.schemaLocation', "$projectDir/schemas".toString()
|
}
|
||||||
}
|
androidResources {
|
||||||
|
generateLocaleConfig true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -33,7 +33,6 @@ android {
|
|||||||
applicationIdSuffix = '.debug'
|
applicationIdSuffix = '.debug'
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
multiDexEnabled false
|
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
@@ -41,17 +40,19 @@ 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/'
|
main.java.srcDirs += 'src/main/kotlin/'
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
coreLibraryDesugaringEnabled true
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
freeCompilerArgs += [
|
freeCompilerArgs += [
|
||||||
'-opt-in=kotlin.ExperimentalStdlibApi',
|
'-opt-in=kotlin.ExperimentalStdlibApi',
|
||||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
@@ -81,33 +82,31 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:07df5a81cf') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:e03d0efe71') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.activity:activity-ktx:1.7.2'
|
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
|
|
||||||
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.3.0'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
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.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
|
||||||
|
|
||||||
// TODO https://issuetracker.google.com/issues/254846063
|
implementation 'androidx.work:work-runtime:2.9.0'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.google.guava:guava:32.0.1-android') {
|
implementation('com.google.guava:guava:32.0.1-android') {
|
||||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||||
@@ -115,48 +114,47 @@ dependencies {
|
|||||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.5.2'
|
implementation 'androidx.room:room-runtime:2.6.1'
|
||||||
implementation 'androidx.room:room-ktx:2.5.2'
|
implementation 'androidx.room:room-ktx:2.6.1'
|
||||||
//noinspection KaptUsageInsteadOfKsp
|
ksp 'androidx.room:room-compiler:2.6.1'
|
||||||
kapt 'androidx.room:room-compiler:2.5.2'
|
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.7.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 'com.google.dagger:hilt-android:2.46.1'
|
implementation 'com.google.dagger:hilt-android:2.50'
|
||||||
kapt 'com.google.dagger:hilt-compiler:2.46.1'
|
kapt 'com.google.dagger:hilt-compiler:2.50'
|
||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.1.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.1.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
implementation 'io.coil-kt:coil-base:2.5.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.4.0'
|
implementation 'io.coil-kt:coil-svg:2.5.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:02e6d6cfe9'
|
||||||
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 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
implementation 'ch.acra:acra-http:5.10.1'
|
implementation 'ch.acra:acra-http:5.11.3'
|
||||||
implementation 'ch.acra:acra-dialog:5.10.1'
|
implementation 'ch.acra:acra-dialog:5.11.3'
|
||||||
|
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20230618'
|
testImplementation 'org.json:json:20231013'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||||
|
|
||||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
|
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.room:room-testing:2.5.2'
|
androidTestImplementation 'androidx.room:room-testing:2.6.1'
|
||||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||||
|
|
||||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.50'
|
||||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
|
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.50'
|
||||||
}
|
}
|
||||||
|
|||||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -18,3 +18,6 @@
|
|||||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||||
|
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
|
||||||
|
-keep class org.jsoup.parser.Tag
|
||||||
|
-keep class org.jsoup.internal.StringUtil
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
@@ -48,6 +48,7 @@ class AppShortcutManagerTest {
|
|||||||
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(
|
||||||
@@ -82,7 +82,7 @@ class AppBackupAgentTest {
|
|||||||
assertEquals(history, historyRepository.getOne(SampleData.manga))
|
assertEquals(history, historyRepository.getOne(SampleData.manga))
|
||||||
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
||||||
|
|
||||||
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
|
val allTags = database.getTagsDao().findTags(SampleData.tag.source.name).toMangaTags()
|
||||||
assertTrue(SampleData.tag in allTags)
|
assertTrue(SampleData.tag in allTags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
|
||||||
|
|
||||||
class LoggingAdapterDataObserver(
|
|
||||||
private val tag: String,
|
|
||||||
) : AdapterDataObserver() {
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
Log.d(tag, "onChanged()")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
|
||||||
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
|
||||||
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
|
||||||
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
|
||||||
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateRestorationPolicyChanged() {
|
|
||||||
Log.d(tag, "onStateRestorationPolicyChanged()")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
@@ -17,21 +18,16 @@ import java.util.EnumSet
|
|||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain()
|
get() = ConfigKey.Domain("")
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val availableSortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getList(
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
offset: Int,
|
|
||||||
query: String?,
|
|
||||||
tags: Set<MangaTag>?,
|
|
||||||
sortOrder: SortOrder,
|
|
||||||
): List<Manga> {
|
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +35,7 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTags(): Set<MangaTag> {
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_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.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="29" />
|
||||||
@@ -46,7 +48,6 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:localeConfig="@xml/locales"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@@ -71,6 +72,27 @@
|
|||||||
<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>
|
||||||
|
<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="manga" />
|
||||||
|
<data android:host="kotatsu.app" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
|
||||||
@@ -84,7 +106,12 @@
|
|||||||
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
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
android:name="org.koitharu.kotatsu.history.ui.HistoryActivity"
|
||||||
android:label="@string/history" />
|
android:label="@string/history" />
|
||||||
@@ -100,6 +127,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
|
||||||
android:label="@string/suggestions" />
|
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"
|
||||||
@@ -124,6 +154,7 @@
|
|||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
@@ -132,16 +163,23 @@
|
|||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
|
||||||
android:label="@string/favourites"
|
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">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetConfigActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/recent_manga">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
|
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
|
||||||
android:label="@string/search" />
|
android:label="@string/search" />
|
||||||
@@ -164,9 +202,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
|
||||||
android:label="@string/color_correction" />
|
android:label="@string/color_correction" />
|
||||||
<activity
|
|
||||||
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
|
|
||||||
android:label="@string/settings" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -186,8 +221,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
|
||||||
|
android:label="@string/sources_catalog" />
|
||||||
|
|
||||||
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
tools:node="merge" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
<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" />
|
||||||
@@ -289,6 +333,20 @@
|
|||||||
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>
|
||||||
|
<receiver
|
||||||
|
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${applicationId}.action.REPORT_ERROR" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
@@ -300,6 +358,660 @@
|
|||||||
android:name="com.samsung.android.icon_container.has_icon_container"
|
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||||
android:value="@bool/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>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Delete
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||||
|
|
||||||
@@ -14,15 +15,24 @@ abstract class BookmarksDao {
|
|||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||||
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
||||||
|
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAll(): Map<MangaWithTags, List<BookmarkEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page ORDER BY percent")
|
||||||
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
||||||
|
|
||||||
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY percent")
|
||||||
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at",
|
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
|
||||||
)
|
)
|
||||||
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||||
|
|
||||||
@@ -35,6 +45,12 @@ abstract class BookmarksDao {
|
|||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||||
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
||||||
|
abstract suspend fun delete(pageId: Long): Int
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
||||||
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
|
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
abstract suspend fun upsert(bookmarks: Collection<BookmarkEntity>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.bookmarks.data
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.*
|
import java.time.Instant
|
||||||
|
|
||||||
fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
|
fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
@@ -11,7 +11,7 @@ fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
|
|||||||
page = page,
|
page = page,
|
||||||
scroll = scroll,
|
scroll = scroll,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
createdAt = Date(createdAt),
|
createdAt = Instant.ofEpochMilli(createdAt),
|
||||||
percent = percent,
|
percent = percent,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ fun Bookmark.toEntity() = BookmarkEntity(
|
|||||||
page = page,
|
page = page,
|
||||||
scroll = scroll,
|
scroll = scroll,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
createdAt = createdAt.time,
|
createdAt = createdAt.toEpochMilli(),
|
||||||
percent = percent,
|
percent = percent,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,4 +30,5 @@ fun Collection<BookmarkEntity>.toBookmarks(manga: Manga) = map {
|
|||||||
it.toBookmark(manga)
|
it.toBookmark(manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Collection<Bookmark>.ids() = map { it.pageId }
|
@JvmName("bookmarksIds")
|
||||||
|
fun Collection<Bookmark>.ids() = map { it.pageId }
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.domain
|
package org.koitharu.kotatsu.bookmarks.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.local.data.hasImageExtension
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import java.util.Date
|
import java.time.Instant
|
||||||
|
|
||||||
class Bookmark(
|
data class Bookmark(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val pageId: Long,
|
val pageId: Long,
|
||||||
val chapterId: Long,
|
val chapterId: Long,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val scroll: Int,
|
val scroll: Int,
|
||||||
val imageUrl: String,
|
val imageUrl: String,
|
||||||
val createdAt: Date,
|
val createdAt: Instant,
|
||||||
val percent: Float,
|
val percent: Float,
|
||||||
) {
|
) : ListModel {
|
||||||
|
|
||||||
val directImageUrl: String?
|
val directImageUrl: String?
|
||||||
get() = if (isImageUrlDirect()) imageUrl else null
|
get() = if (isImageUrlDirect()) imageUrl else null
|
||||||
|
|
||||||
|
val imageLoadData: Any
|
||||||
|
get() = if (isImageUrlDirect()) imageUrl else toMangaPage()
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is Bookmark &&
|
||||||
|
manga.id == other.manga.id &&
|
||||||
|
chapterId == other.chapterId &&
|
||||||
|
page == other.page
|
||||||
|
}
|
||||||
|
|
||||||
fun toMangaPage() = MangaPage(
|
fun toMangaPage() = MangaPage(
|
||||||
id = pageId,
|
id = pageId,
|
||||||
url = imageUrl,
|
url = imageUrl,
|
||||||
@@ -26,34 +38,6 @@ class Bookmark(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private fun isImageUrlDirect(): Boolean {
|
private fun isImageUrlDirect(): Boolean {
|
||||||
return imageUrl.substringAfterLast('.').length in 2..4
|
return hasImageExtension(imageUrl)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
return percent == other.percent
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ class BookmarksRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> {
|
fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> {
|
||||||
return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
|
return db.getBookmarksDao().observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> {
|
fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> {
|
||||||
return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) }
|
return db.getBookmarksDao().observe(manga.id).mapItems { it.toBookmark(manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> {
|
fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> {
|
||||||
return db.bookmarksDao.observe().map { map ->
|
return db.getBookmarksDao().observe().map { map ->
|
||||||
val res = LinkedHashMap<Manga, List<Bookmark>>(map.size)
|
val res = LinkedHashMap<Manga, List<Bookmark>>(map.size)
|
||||||
for ((k, v) in map) {
|
for ((k, v) in map) {
|
||||||
val manga = k.toManga()
|
val manga = k.toManga()
|
||||||
@@ -46,14 +46,21 @@ class BookmarksRepository @Inject constructor(
|
|||||||
suspend fun addBookmark(bookmark: Bookmark) {
|
suspend fun addBookmark(bookmark: Bookmark) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val tags = bookmark.manga.tags.toEntities()
|
val tags = bookmark.manga.tags.toEntities()
|
||||||
db.tagsDao.upsert(tags)
|
db.getTagsDao().upsert(tags)
|
||||||
db.mangaDao.upsert(bookmark.manga.toEntity(), tags)
|
db.getMangaDao().upsert(bookmark.manga.toEntity(), tags)
|
||||||
db.bookmarksDao.insert(bookmark.toEntity())
|
db.getBookmarksDao().insert(bookmark.toEntity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateBookmark(bookmark: Bookmark, imageUrl: String) {
|
||||||
|
val entity = bookmark.toEntity().copy(
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
)
|
||||||
|
db.getBookmarksDao().upsert(listOf(entity))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
|
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
|
||||||
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
|
check(db.getBookmarksDao().delete(mangaId, chapterId, page) != 0) {
|
||||||
"Bookmark not found"
|
"Bookmark not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,18 +69,16 @@ class BookmarksRepository @Inject constructor(
|
|||||||
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
|
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
suspend fun removeBookmarks(ids: Set<Long>): ReversibleHandle {
|
||||||
val entities = ArrayList<BookmarkEntity>(ids.size)
|
val entities = ArrayList<BookmarkEntity>(ids.size)
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val dao = db.bookmarksDao
|
val dao = db.getBookmarksDao()
|
||||||
for ((manga, idSet) in ids) {
|
for (pageId in ids) {
|
||||||
for (pageId in idSet) {
|
val e = dao.find(pageId)
|
||||||
val e = dao.find(manga.id, pageId)
|
if (e != null) {
|
||||||
if (e != null) {
|
entities.add(e)
|
||||||
entities.add(e)
|
|
||||||
}
|
|
||||||
dao.delete(manga.id, pageId)
|
|
||||||
}
|
}
|
||||||
|
dao.delete(pageId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BookmarksRestorer(entities)
|
return BookmarksRestorer(entities)
|
||||||
@@ -87,7 +92,7 @@ class BookmarksRepository @Inject constructor(
|
|||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
for (e in entities) {
|
for (e in entities) {
|
||||||
try {
|
try {
|
||||||
db.bookmarksDao.insert(e)
|
db.getBookmarksDao().insert(e)
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class BookmarksActivity :
|
|||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
if (fm.findFragmentById(R.id.container) == null) {
|
if (fm.findFragmentById(R.id.container) == null) {
|
||||||
fm.commit {
|
fm.commit {
|
||||||
val fragment = BookmarksFragment.newInstance()
|
setReorderingAllowed(true)
|
||||||
replace(R.id.container, fragment)
|
replace(R.id.container, BookmarksFragment::class.java, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,33 +12,30 @@ import androidx.core.graphics.Insets
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.bookmarks.data.ids
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
|
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.util.reverseAsync
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -48,64 +45,90 @@ class BookmarksFragment :
|
|||||||
BaseFragment<FragmentListSimpleBinding>(),
|
BaseFragment<FragmentListSimpleBinding>(),
|
||||||
ListStateHolderListener,
|
ListStateHolderListener,
|
||||||
OnListItemClickListener<Bookmark>,
|
OnListItemClickListener<Bookmark>,
|
||||||
SectionedSelectionController.Callback<Manga>,
|
ListSelectionController.Callback2,
|
||||||
FastScroller.FastScrollListener {
|
FastScroller.FastScrollListener, ListHeaderClickListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
private val viewModel by viewModels<BookmarksViewModel>()
|
@Inject
|
||||||
private var adapter: BookmarksGroupAdapter? = null
|
lateinit var settings: AppSettings
|
||||||
private var selectionController: SectionedSelectionController<Manga>? = null
|
|
||||||
|
|
||||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
|
private val viewModel by viewModels<BookmarksViewModel>()
|
||||||
|
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||||
|
private var selectionController: ListSelectionController? = null
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
): FragmentListSimpleBinding {
|
||||||
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
return FragmentListSimpleBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(
|
||||||
|
binding: FragmentListSimpleBinding,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
selectionController = SectionedSelectionController(
|
selectionController = ListSelectionController(
|
||||||
activity = requireActivity(),
|
activity = requireActivity(),
|
||||||
owner = this,
|
decoration = BookmarksSelectionDecoration(binding.root.context),
|
||||||
|
registryOwner = this,
|
||||||
callback = this,
|
callback = this,
|
||||||
)
|
)
|
||||||
adapter = BookmarksGroupAdapter(
|
bookmarksAdapter = BookmarksAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
coil = coil,
|
coil = coil,
|
||||||
listener = this,
|
clickListener = this,
|
||||||
selectionController = checkNotNull(selectionController),
|
headerClickListener = this,
|
||||||
bookmarkClickListener = this,
|
|
||||||
groupClickListener = OnGroupClickListener(),
|
|
||||||
)
|
)
|
||||||
binding.recyclerView.adapter = adapter
|
val spanSizeLookup = SpanSizeLookup()
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
with(binding.recyclerView) {
|
||||||
val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
setHasFixedSize(true)
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
val spanResolver = MangaListSpanResolver(resources)
|
||||||
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
adapter = bookmarksAdapter
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
addOnLayoutChangeListener(spanResolver)
|
||||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
|
spanResolver.setGridSize(settings.gridSize / 100f, this)
|
||||||
|
val lm = GridLayoutManager(context, spanResolver.spanCount)
|
||||||
|
lm.spanSizeLookup = spanSizeLookup
|
||||||
|
layoutManager = lm
|
||||||
|
selectionController?.attachToRecyclerView(this)
|
||||||
|
}
|
||||||
|
viewModel.content.observe(viewLifecycleOwner) {
|
||||||
|
bookmarksAdapter?.setItems(it, spanSizeLookup)
|
||||||
|
}
|
||||||
|
viewModel.onError.observeEvent(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
SnackbarErrorObserver(binding.recyclerView, this)
|
||||||
|
)
|
||||||
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
adapter = null
|
bookmarksAdapter = null
|
||||||
selectionController = null
|
selectionController = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: Bookmark, view: View) {
|
override fun onItemClick(item: Bookmark, view: View) {
|
||||||
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
|
if (selectionController?.onItemClick(item.pageId) != true) {
|
||||||
val intent = ReaderActivity.IntentBuilder(view.context)
|
val intent = ReaderActivity.IntentBuilder(view.context)
|
||||||
.bookmark(item)
|
.bookmark(item)
|
||||||
.incognito(true)
|
.incognito(true)
|
||||||
.build()
|
.build()
|
||||||
startActivity(intent, scaleUpActivityOptionsOf(view))
|
startActivity(intent)
|
||||||
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onListHeaderClick(item: ListHeader, view: View) {
|
||||||
|
val manga = item.payload as? Manga ?: return
|
||||||
|
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
|
||||||
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
|
return selectionController?.onItemLongClick(item.pageId) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) = Unit
|
||||||
@@ -118,12 +141,12 @@ class BookmarksFragment :
|
|||||||
|
|
||||||
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
|
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
|
||||||
|
|
||||||
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
|
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
|
||||||
requireViewBinding().recyclerView.invalidateNestedItemDecorations()
|
requireViewBinding().recyclerView.invalidateItemDecorations()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateActionMode(
|
override fun onCreateActionMode(
|
||||||
controller: SectionedSelectionController<Manga>,
|
controller: ListSelectionController,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
menu: Menu,
|
menu: Menu,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -132,7 +155,7 @@ class BookmarksFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(
|
override fun onActionItemClicked(
|
||||||
controller: SectionedSelectionController<Manga>,
|
controller: ListSelectionController,
|
||||||
mode: ActionMode,
|
mode: ActionMode,
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -148,57 +171,46 @@ class BookmarksFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateItemDecoration(
|
|
||||||
controller: SectionedSelectionController<Manga>,
|
|
||||||
section: Manga,
|
|
||||||
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
|
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
requireViewBinding().recyclerView.updatePadding(
|
val rv = requireViewBinding().recyclerView
|
||||||
bottom = insets.bottom,
|
rv.updatePadding(
|
||||||
|
bottom = insets.bottom + rv.paddingTop,
|
||||||
)
|
)
|
||||||
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
rv.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
bottomMargin = insets.bottom
|
bottomMargin = insets.bottom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListChanged(list: List<ListModel>) {
|
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable {
|
||||||
adapter?.items = list
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onActionDone(action: ReversibleAction) {
|
init {
|
||||||
val handle = action.handle
|
isSpanIndexCacheEnabled = true
|
||||||
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
|
isSpanGroupIndexCacheEnabled = true
|
||||||
val snackbar = Snackbar.make((activity as SnackbarOwner).snackbarHost, action.stringResId, length)
|
|
||||||
if (handle != null) {
|
|
||||||
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
|
|
||||||
}
|
}
|
||||||
snackbar.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount
|
||||||
override fun onItemClick(item: BookmarksGroup, view: View) {
|
?: return 1
|
||||||
val controller = selectionController
|
return when (bookmarksAdapter?.getItemViewType(position)) {
|
||||||
if (controller != null && controller.count > 0) {
|
ListItemType.PAGE_THUMB.ordinal -> 1
|
||||||
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
|
else -> total
|
||||||
controller.clearSelection(item.manga)
|
|
||||||
} else {
|
|
||||||
controller.addToSelection(item.manga, item.bookmarks.ids())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val intent = DetailsActivity.newIntent(view.context, item.manga)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
|
override fun run() {
|
||||||
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
|
invalidateSpanGroupIndexCache()
|
||||||
|
invalidateSpanIndexCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"", ReplaceWith(
|
||||||
|
"BookmarksFragment()",
|
||||||
|
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment"
|
||||||
|
)
|
||||||
|
)
|
||||||
fun newInstance() = BookmarksFragment()
|
fun newInstance() = BookmarksFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
@@ -41,17 +42,26 @@ class BookmarksViewModel @Inject constructor(
|
|||||||
actionStringRes = 0,
|
actionStringRes = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else list.map { (manga, bookmarks) ->
|
} else {
|
||||||
BookmarksGroup(manga, bookmarks)
|
mapList(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
|
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
|
fun removeBookmarks(ids: Set<Long>) {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val handle = repository.removeBookmarks(ids)
|
val handle = repository.removeBookmarks(ids)
|
||||||
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
|
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mapList(data: Map<Manga, List<Bookmark>>): List<ListModel> {
|
||||||
|
val result = ArrayList<ListModel>(data.values.sumOf { it.size + 1 })
|
||||||
|
for ((manga, bookmarks) in data) {
|
||||||
|
result.add(ListHeader(manga.title, R.string.more, manga))
|
||||||
|
result.addAll(bookmarks)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
|||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
@@ -28,20 +27,16 @@ fun bookmarkListAD(
|
|||||||
binding.root.setOnLongClickListener(listener)
|
binding.root.setOnLongClickListener(listener)
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
val data: Any = item.directImageUrl ?: item.toMangaPage()
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||||
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
|
|
||||||
size(CoverSizeResolver(binding.imageViewThumb))
|
size(CoverSizeResolver(binding.imageViewThumb))
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(R.drawable.ic_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
error(R.drawable.ic_error_placeholder)
|
error(R.drawable.ic_error_placeholder)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
tag(item)
|
||||||
decodeRegion(item.scroll)
|
decodeRegion(item.scroll)
|
||||||
source(item.manga.source)
|
source(item.manga.source)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
binding.imageViewThumb.disposeImageRequest()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
|
||||||
class BookmarksAdapter(
|
class BookmarksAdapter(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
clickListener: OnListItemClickListener<Bookmark>,
|
clickListener: OnListItemClickListener<Bookmark>,
|
||||||
) : AsyncListDifferDelegationAdapter<Bookmark>(
|
) : BaseListAdapter<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 &&
|
|
||||||
oldItem.page == newItem.page
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
|
||||||
return oldItem.imageUrl == newItem.imageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
|
|
||||||
fun bookmarksGroupAD(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
sharedPool: RecyclerView.RecycledViewPool,
|
|
||||||
selectionController: SectionedSelectionController<Manga>,
|
|
||||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
|
||||||
) = adapterDelegateViewBinding<BookmarksGroup, ListModel, ItemBookmarksGroupBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemBookmarksGroupBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
val viewListenerAdapter = object : View.OnClickListener, View.OnLongClickListener {
|
|
||||||
override fun onClick(v: View) = groupClickListener.onItemClick(item, v)
|
|
||||||
override fun onLongClick(v: View) = groupClickListener.onItemLongClick(item, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter = BookmarksAdapter(coil, lifecycleOwner, bookmarkClickListener)
|
|
||||||
binding.recyclerView.setRecycledViewPool(sharedPool)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
binding.root.setOnClickListener(viewListenerAdapter)
|
|
||||||
binding.root.setOnLongClickListener(viewListenerAdapter)
|
|
||||||
|
|
||||||
bind { payloads ->
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
binding.recyclerView.clearItemDecorations()
|
|
||||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
|
||||||
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
|
|
||||||
}
|
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
|
|
||||||
placeholder(R.drawable.ic_placeholder)
|
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
allowRgb565(true)
|
|
||||||
size(CoverSizeResolver(binding.imageViewCover))
|
|
||||||
source(item.manga.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
binding.textViewTitle.text = item.manga.title
|
|
||||||
adapter.items = item.bookmarks
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
binding.imageViewCover.disposeImageRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.adapter
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import kotlin.jvm.internal.Intrinsics
|
|
||||||
|
|
||||||
class BookmarksGroupAdapter(
|
|
||||||
coil: ImageLoader,
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
selectionController: SectionedSelectionController<Manga>,
|
|
||||||
listener: ListStateHolderListener,
|
|
||||||
bookmarkClickListener: OnListItemClickListener<Bookmark>,
|
|
||||||
groupClickListener: OnListItemClickListener<BookmarksGroup>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pool = RecyclerView.RecycledViewPool()
|
|
||||||
delegatesManager
|
|
||||||
.addDelegate(
|
|
||||||
bookmarksGroupAD(
|
|
||||||
coil = coil,
|
|
||||||
lifecycleOwner = lifecycleOwner,
|
|
||||||
sharedPool = pool,
|
|
||||||
selectionController = selectionController,
|
|
||||||
bookmarkClickListener = bookmarkClickListener,
|
|
||||||
groupClickListener = groupClickListener,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.addDelegate(loadingStateAD())
|
|
||||||
.addDelegate(loadingFooterAD())
|
|
||||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
|
|
||||||
.addDelegate(errorStateListAD(listener))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> {
|
|
||||||
oldItem.manga.id == newItem.manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is LoadingFooter && newItem is LoadingFooter -> {
|
|
||||||
oldItem.key == newItem.key
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> oldItem.javaClass == newItem.javaClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
|
||||||
return Intrinsics.areEqual(oldItem, newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
|
||||||
return when {
|
|
||||||
oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit
|
|
||||||
else -> super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.ui.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.util.areItemsEquals
|
|
||||||
|
|
||||||
class BookmarksGroup(
|
|
||||||
val manga: Manga,
|
|
||||||
val bookmarks: List<Bookmark>,
|
|
||||||
) : ListModel {
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as BookmarksGroup
|
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
|
|
||||||
return bookmarks.areItemsEquals(other.bookmarks) { a, b ->
|
|
||||||
a.imageUrl == b.imageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = manga.hashCode()
|
|
||||||
result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
fun bookmarkLargeAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
clickListener: OnListItemClickListener<Bookmark>,
|
||||||
|
) = adapterDelegateViewBinding<Bookmark, ListModel, ItemBookmarkLargeBinding>(
|
||||||
|
{ inflater, parent -> ItemBookmarkLargeBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
val listener = AdapterDelegateClickListenerAdapter(this, clickListener)
|
||||||
|
|
||||||
|
binding.root.setOnClickListener(listener)
|
||||||
|
binding.root.setOnLongClickListener(listener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
|
||||||
|
size(CoverSizeResolver(binding.imageViewThumb))
|
||||||
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
allowRgb565(true)
|
||||||
|
tag(item)
|
||||||
|
decodeRegion(item.scroll)
|
||||||
|
source(item.manga.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
binding.progressView.percent = item.percent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class BookmarksAdapter(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
clickListener: OnListItemClickListener<Bookmark>,
|
||||||
|
headerClickListener: ListHeaderClickListener?,
|
||||||
|
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
|
init {
|
||||||
|
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
|
||||||
|
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
|
||||||
|
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
|
||||||
|
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||||
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
val list = items
|
||||||
|
for (i in (0..position).reversed()) {
|
||||||
|
val item = list.getOrNull(i) ?: continue
|
||||||
|
if (item is ListHeader) {
|
||||||
|
return item.getText(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import coil.ImageLoader
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||||
|
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.plus
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.showDistinct
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
|
import org.koitharu.kotatsu.databinding.SheetPagesBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||||
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
|
||||||
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class BookmarksSheet :
|
||||||
|
BaseAdaptiveSheet<SheetPagesBinding>(),
|
||||||
|
AdaptiveSheetCallback,
|
||||||
|
OnListItemClickListener<Bookmark> {
|
||||||
|
|
||||||
|
private val viewModel by viewModels<BookmarksSheetViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settings: AppSettings
|
||||||
|
|
||||||
|
private var bookmarksAdapter: BookmarksAdapter? = null
|
||||||
|
private var spanResolver: MangaListSpanResolver? = null
|
||||||
|
|
||||||
|
private val spanSizeLookup = SpanSizeLookup()
|
||||||
|
private val listCommitCallback = Runnable {
|
||||||
|
spanSizeLookup.invalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
|
||||||
|
return SheetPagesBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
addSheetCallback(this)
|
||||||
|
spanResolver = MangaListSpanResolver(binding.root.resources)
|
||||||
|
bookmarksAdapter = BookmarksAdapter(
|
||||||
|
coil = coil,
|
||||||
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
|
clickListener = this@BookmarksSheet,
|
||||||
|
headerClickListener = null,
|
||||||
|
)
|
||||||
|
viewBinding?.headerBar?.setTitle(R.string.bookmarks)
|
||||||
|
with(binding.recyclerView) {
|
||||||
|
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||||
|
adapter = bookmarksAdapter
|
||||||
|
addOnLayoutChangeListener(spanResolver)
|
||||||
|
spanResolver?.setGridSize(settings.gridSize / 100f, this)
|
||||||
|
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
|
||||||
|
}
|
||||||
|
viewModel.content.observe(viewLifecycleOwner, ::onThumbnailsChanged)
|
||||||
|
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
spanResolver = null
|
||||||
|
bookmarksAdapter = null
|
||||||
|
spanSizeLookup.invalidateCache()
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: Bookmark, view: View) {
|
||||||
|
val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener)
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onPageSelected(ReaderPage(item.toMangaPage(), item.page, item.chapterId))
|
||||||
|
} else {
|
||||||
|
val intent = IntentBuilder(view.context)
|
||||||
|
.manga(viewModel.manga)
|
||||||
|
.bookmark(item)
|
||||||
|
.incognito(true)
|
||||||
|
.build()
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(sheet: View, newState: Int) {
|
||||||
|
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onThumbnailsChanged(list: List<ListModel>) {
|
||||||
|
val adapter = bookmarksAdapter ?: return
|
||||||
|
if (adapter.itemCount == 0) {
|
||||||
|
var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent }
|
||||||
|
if (position > 0) {
|
||||||
|
val spanCount = spanResolver?.spanCount ?: 0
|
||||||
|
val offset = if (position > spanCount + 1) {
|
||||||
|
(resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt()
|
||||||
|
} else {
|
||||||
|
position = 0
|
||||||
|
0
|
||||||
|
}
|
||||||
|
val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset)
|
||||||
|
adapter.setItems(list, listCommitCallback + scrollCallback)
|
||||||
|
} else {
|
||||||
|
adapter.setItems(list, listCommitCallback)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
adapter.setItems(list, listCommitCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
isSpanIndexCacheEnabled = true
|
||||||
|
isSpanGroupIndexCacheEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||||
|
return when (bookmarksAdapter?.getItemViewType(position)) {
|
||||||
|
ListItemType.PAGE_THUMB.ordinal -> 1
|
||||||
|
else -> total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
invalidateSpanGroupIndexCache()
|
||||||
|
invalidateSpanIndexCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ARG_MANGA = "manga"
|
||||||
|
|
||||||
|
private const val TAG = "BookmarksSheet"
|
||||||
|
|
||||||
|
fun show(fm: FragmentManager, manga: Manga) {
|
||||||
|
BookmarksSheet().withArgs(1) {
|
||||||
|
putParcelable(ARG_MANGA, ParcelableManga(manga))
|
||||||
|
}.showDistinct(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.koitharu.kotatsu.bookmarks.ui.sheet
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
|
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.require
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class BookmarksSheetViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
bookmarksRepository: BookmarksRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val manga = savedStateHandle.require<ParcelableManga>(BookmarksSheet.ARG_MANGA).manga
|
||||||
|
private val chaptersLazy = SuspendLazy {
|
||||||
|
requireNotNull(manga.chapters ?: mangaRepositoryFactory.create(manga.source).getDetails(manga).chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
val content: StateFlow<List<ListModel>> = bookmarksRepository.observeBookmarks(manga)
|
||||||
|
.map { mapList(it) }
|
||||||
|
.withErrorHandling()
|
||||||
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingFooter()))
|
||||||
|
|
||||||
|
private suspend fun mapList(bookmarks: List<Bookmark>): List<ListModel> {
|
||||||
|
val chapters = chaptersLazy.get()
|
||||||
|
val bookmarksMap = bookmarks.groupBy { it.chapterId }
|
||||||
|
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)
|
||||||
|
for (chapter in chapters) {
|
||||||
|
val b = bookmarksMap[chapter.id]
|
||||||
|
if (b.isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result += ListHeader(chapter.name)
|
||||||
|
result.addAll(b)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import androidx.core.graphics.Insets
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import coil.EventListener
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class CaptchaNotifier(
|
||||||
|
private val context: Context,
|
||||||
|
) : EventListener {
|
||||||
|
|
||||||
|
fun notify(exception: CloudFlareProtectedException) {
|
||||||
|
if (!context.checkNotificationPermission()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val manager = NotificationManagerCompat.from(context)
|
||||||
|
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||||
|
.setName(context.getString(R.string.captcha_required))
|
||||||
|
.setShowBadge(true)
|
||||||
|
.setVibrationEnabled(false)
|
||||||
|
.setSound(null, null)
|
||||||
|
.setLightsEnabled(false)
|
||||||
|
.build()
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
|
||||||
|
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers)
|
||||||
|
.setData(exception.url.toUri())
|
||||||
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setContentTitle(channel.name)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setVisibility(
|
||||||
|
if (exception.source?.contentType == ContentType.HENTAI) {
|
||||||
|
NotificationCompat.VISIBILITY_SECRET
|
||||||
|
} else {
|
||||||
|
NotificationCompat.VISIBILITY_PUBLIC
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.setContentText(
|
||||||
|
context.getString(
|
||||||
|
R.string.captcha_required_summary,
|
||||||
|
exception.source?.title ?: context.getString(R.string.app_name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
|
||||||
|
.build()
|
||||||
|
manager.notify(TAG, exception.source.hashCode(), notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss(source: MangaSource) {
|
||||||
|
NotificationManagerCompat.from(context).cancel(TAG, source.hashCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
|
super.onError(request, result)
|
||||||
|
val e = result.throwable
|
||||||
|
if (e is CloudFlareProtectedException && request.parameters.value<Boolean>(PARAM_IGNORE_CAPTCHA) != true) {
|
||||||
|
notify(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun ImageRequest.Builder.ignoreCaptchaErrors() = setParameter(
|
||||||
|
key = PARAM_IGNORE_CAPTCHA,
|
||||||
|
value = true,
|
||||||
|
memoryCacheKey = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
|
||||||
|
private const val CHANNEL_ID = "captcha"
|
||||||
|
private const val TAG = CHANNEL_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,22 +3,27 @@ package org.koitharu.kotatsu.browser.cloudflare
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebSettings
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
import org.koitharu.kotatsu.core.util.TaggedActivityResult
|
||||||
@@ -40,7 +45,13 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
if (!catchingWebViewUnavailability {
|
||||||
|
setContentView(
|
||||||
|
ActivityBrowserBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
@@ -71,9 +82,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
viewBinding.webView.run {
|
runCatching {
|
||||||
stopLoading()
|
viewBinding.webView
|
||||||
destroy()
|
}.onSuccess {
|
||||||
|
it.stopLoading()
|
||||||
|
it.destroy()
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -88,6 +101,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
viewBinding.webView.restoreState(savedInstanceState)
|
viewBinding.webView.restoreState(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.opt_captcha, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
viewBinding.appbar.updatePadding(
|
viewBinding.appbar.updatePadding(
|
||||||
top = insets.top,
|
top = insets.top,
|
||||||
@@ -106,6 +124,19 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.action_retry -> {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewBinding.webView.stopLoading()
|
||||||
|
yield()
|
||||||
|
val targetUrl = intent?.dataString?.toHttpUrlOrNull()
|
||||||
|
if (targetUrl != null) {
|
||||||
|
clearCfCookies(targetUrl)
|
||||||
|
viewBinding.webView.loadUrl(targetUrl.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +174,15 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
|||||||
|
|
||||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||||
setTitle(title)
|
setTitle(title)
|
||||||
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
supportActionBar?.subtitle =
|
||||||
|
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) {
|
||||||
|
cookieJar.removeCookies(url) { cookie ->
|
||||||
|
val name = cookie.name
|
||||||
|
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.provider.SearchRecentSuggestions
|
|||||||
import android.text.Html
|
import android.text.Html
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
|
import androidx.work.WorkManager
|
||||||
import coil.ComponentRegistry
|
import coil.ComponentRegistry
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
@@ -24,11 +25,13 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.network.*
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||||
@@ -38,13 +41,13 @@ import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
|
|||||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||||
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
||||||
import org.koitharu.kotatsu.core.util.ext.activityManager
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
|
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||||
@@ -88,6 +91,7 @@ interface AppModule {
|
|||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
imageProxyInterceptor: ImageProxyInterceptor,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
|
coverRestoreInterceptor: CoverRestoreInterceptor,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val diskCacheFactory = {
|
val diskCacheFactory = {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
@@ -104,6 +108,7 @@ interface AppModule {
|
|||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
.allowRgb565(context.isLowRamDevice())
|
.allowRgb565(context.isLowRamDevice())
|
||||||
|
.eventListener(CaptchaNotifier(context))
|
||||||
.components(
|
.components(
|
||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
@@ -111,6 +116,7 @@ interface AppModule {
|
|||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
.add(pageFetcherFactory)
|
.add(pageFetcherFactory)
|
||||||
.add(imageProxyInterceptor)
|
.add(imageProxyInterceptor)
|
||||||
|
.add(coverRestoreInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
@@ -155,7 +161,7 @@ interface AppModule {
|
|||||||
fun provideContentCache(
|
fun provideContentCache(
|
||||||
application: Application,
|
application: Application,
|
||||||
): ContentCache {
|
): ContentCache {
|
||||||
return if (application.activityManager?.isLowRamDevice == true) {
|
return if (application.isLowRamDevice()) {
|
||||||
StubContentCache()
|
StubContentCache()
|
||||||
} else {
|
} else {
|
||||||
MemoryContentCache(application)
|
MemoryContentCache(application)
|
||||||
@@ -172,5 +178,10 @@ interface AppModule {
|
|||||||
fun provideLocalStorageChangesFlow(
|
fun provideLocalStorageChangesFlow(
|
||||||
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
|
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
|
||||||
): SharedFlow<LocalManga?> = flow.asSharedFlow()
|
): SharedFlow<LocalManga?> = flow.asSharedFlow()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideWorkManager(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): WorkManager = WorkManager.getInstance(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package org.koitharu.kotatsu
|
package org.koitharu.kotatsu.core
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.StrictMode
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
|
||||||
import androidx.hilt.work.HiltWorkerFactory
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.ReportField
|
import org.acra.ReportField
|
||||||
import org.acra.config.dialog
|
import org.acra.config.dialog
|
||||||
@@ -19,20 +19,19 @@ import org.acra.config.httpSender
|
|||||||
import org.acra.data.StringFormat
|
import org.acra.data.StringFormat
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.os.AppValidator
|
import org.koitharu.kotatsu.core.os.AppValidator
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
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
|
|
||||||
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
|
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class KotatsuApp : Application(), Configuration.Provider {
|
open class BaseApp : Application(), Configuration.Provider {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
|
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
|
||||||
@@ -41,7 +40,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
|
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: MangaDatabase
|
lateinit var database: Provider<MangaDatabase>
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var settings: AppSettings
|
lateinit var settings: AppSettings
|
||||||
@@ -53,22 +52,32 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
lateinit var appValidator: AppValidator
|
lateinit var appValidator: AppValidator
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var workScheduleManager: WorkScheduleManager
|
lateinit var workScheduleManager: Provider<WorkScheduleManager>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workManagerProvider: Provider<WorkManager>
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
enableStrictMode()
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||||
setupActivityLifecycleCallbacks()
|
setupActivityLifecycleCallbacks()
|
||||||
|
processLifecycleScope.launch {
|
||||||
|
val isOriginalApp = withContext(Dispatchers.Default) {
|
||||||
|
appValidator.isOriginalApp
|
||||||
|
}
|
||||||
|
ACRA.errorReporter.putCustomData("isOriginalApp", isOriginalApp.toString())
|
||||||
|
}
|
||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
setupDatabaseObservers()
|
setupDatabaseObservers()
|
||||||
}
|
}
|
||||||
workScheduleManager.init()
|
workScheduleManager.get().init()
|
||||||
WorkServiceStopHelper(applicationContext).setup()
|
WorkServiceStopHelper(workManagerProvider).setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
@@ -76,13 +85,6 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
initAcra {
|
initAcra {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
excludeMatchingSharedPreferencesKeys = listOf(
|
|
||||||
"sources_\\w+",
|
|
||||||
AppSettings.KEY_APP_PASSWORD,
|
|
||||||
AppSettings.KEY_PROXY_LOGIN,
|
|
||||||
AppSettings.KEY_PROXY_ADDRESS,
|
|
||||||
AppSettings.KEY_PROXY_PASSWORD,
|
|
||||||
)
|
|
||||||
httpSender {
|
httpSender {
|
||||||
uri = getString(R.string.url_error_report)
|
uri = getString(R.string.url_error_report)
|
||||||
basicAuthLogin = getString(R.string.acra_login)
|
basicAuthLogin = getString(R.string.acra_login)
|
||||||
@@ -99,7 +101,6 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
ReportField.STACK_TRACE,
|
ReportField.STACK_TRACE,
|
||||||
ReportField.CRASH_CONFIGURATION,
|
ReportField.CRASH_CONFIGURATION,
|
||||||
ReportField.CUSTOM_DATA,
|
ReportField.CUSTOM_DATA,
|
||||||
ReportField.SHARED_PREFERENCES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
@@ -112,15 +113,9 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration {
|
|
||||||
return Configuration.Builder()
|
|
||||||
.setWorkerFactory(workerFactory)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun setupDatabaseObservers() {
|
private fun setupDatabaseObservers() {
|
||||||
val tracker = database.invalidationTracker
|
val tracker = database.get().invalidationTracker
|
||||||
databaseObservers.forEach {
|
databaseObservers.forEach {
|
||||||
tracker.addObserver(it)
|
tracker.addObserver(it)
|
||||||
}
|
}
|
||||||
@@ -131,31 +126,4 @@ class KotatsuApp : Application(), Configuration.Provider {
|
|||||||
registerActivityLifecycleCallbacks(it)
|
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)
|
|
||||||
.setClassInstanceLimit(PageLoader::class.java, 1)
|
|
||||||
.penaltyLog()
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
|
|
||||||
.penaltyDeath()
|
|
||||||
.detectFragmentReuse()
|
|
||||||
.detectWrongFragmentContainer()
|
|
||||||
.detectRetainInstanceUsage()
|
|
||||||
.detectSetUserVisibleHint()
|
|
||||||
.detectFragmentTagUsage()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.core
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.report
|
||||||
|
|
||||||
|
class ErrorReporterReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val e = intent?.getSerializableExtraCompat<Throwable>(EXTRA_ERROR) ?: return
|
||||||
|
e.report()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_ERROR = "err"
|
||||||
|
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
|
||||||
|
|
||||||
|
fun getPendingIntent(context: Context, e: Throwable): PendingIntent {
|
||||||
|
val intent = Intent(context, ErrorReporterReceiver::class.java)
|
||||||
|
intent.setAction(ACTION_REPORT)
|
||||||
|
intent.setData(Uri.parse("err://${e.hashCode()}"))
|
||||||
|
intent.putExtra(EXTRA_ERROR, e)
|
||||||
|
return checkNotNull(PendingIntentCompat.getBroadcast(context, 0, intent, 0, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,20 @@ package org.koitharu.kotatsu.core.backup
|
|||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
|
||||||
class BackupEntry(
|
class BackupEntry(
|
||||||
val name: String,
|
val name: Name,
|
||||||
val data: JSONArray
|
val data: JSONArray
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object Names {
|
enum class Name(
|
||||||
|
val key: String,
|
||||||
|
) {
|
||||||
|
|
||||||
const val INDEX = "index"
|
INDEX("index"),
|
||||||
const val HISTORY = "history"
|
HISTORY("history"),
|
||||||
const val CATEGORIES = "categories"
|
CATEGORIES("categories"),
|
||||||
const val FAVOURITES = "favourites"
|
FAVOURITES("favourites"),
|
||||||
|
SETTINGS("settings"),
|
||||||
|
BOOKMARKS("bookmarks"),
|
||||||
|
SOURCES("sources"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,26 @@ import org.json.JSONArray
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 10
|
private const val PAGE_SIZE = 10
|
||||||
|
|
||||||
class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
class BackupRepository @Inject constructor(
|
||||||
|
private val db: MangaDatabase,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) {
|
||||||
|
|
||||||
suspend fun dumpHistory(): BackupEntry {
|
suspend fun dumpHistory(): BackupEntry {
|
||||||
var offset = 0
|
var offset = 0
|
||||||
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
|
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
|
||||||
while (true) {
|
while (true) {
|
||||||
val history = db.historyDao.findAll(offset, PAGE_SIZE)
|
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
|
||||||
if (history.isEmpty()) {
|
if (history.isEmpty()) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -37,8 +43,8 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun dumpCategories(): BackupEntry {
|
suspend fun dumpCategories(): BackupEntry {
|
||||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
|
||||||
val categories = db.favouriteCategoriesDao.findAll()
|
val categories = db.getFavouriteCategoriesDao().findAll()
|
||||||
for (item in categories) {
|
for (item in categories) {
|
||||||
entry.data.put(JsonSerializer(item).toJson())
|
entry.data.put(JsonSerializer(item).toJson())
|
||||||
}
|
}
|
||||||
@@ -47,9 +53,9 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
|
|
||||||
suspend fun dumpFavourites(): BackupEntry {
|
suspend fun dumpFavourites(): BackupEntry {
|
||||||
var offset = 0
|
var offset = 0
|
||||||
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
|
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
|
||||||
while (true) {
|
while (true) {
|
||||||
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE)
|
val favourites = db.getFavouritesDao().findAllRaw(offset, PAGE_SIZE)
|
||||||
if (favourites.isEmpty()) {
|
if (favourites.isEmpty()) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -67,8 +73,48 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun dumpBookmarks(): BackupEntry {
|
||||||
|
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
|
||||||
|
val all = db.getBookmarksDao().findAll()
|
||||||
|
for ((m, b) in all) {
|
||||||
|
val json = JSONObject()
|
||||||
|
val manga = JsonSerializer(m.manga).toJson()
|
||||||
|
json.put("manga", manga)
|
||||||
|
val tags = JSONArray()
|
||||||
|
m.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
|
||||||
|
json.put("tags", tags)
|
||||||
|
val bookmarks = JSONArray()
|
||||||
|
b.forEach { bookmarks.put(JsonSerializer(it).toJson()) }
|
||||||
|
json.put("bookmarks", bookmarks)
|
||||||
|
entry.data.put(json)
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dumpSettings(): BackupEntry {
|
||||||
|
val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
|
||||||
|
val settingsDump = settings.getAllValues().toMutableMap()
|
||||||
|
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
|
||||||
|
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
|
||||||
|
settingsDump.remove(AppSettings.KEY_PROXY_LOGIN)
|
||||||
|
settingsDump.remove(AppSettings.KEY_INCOGNITO_MODE)
|
||||||
|
val json = JsonSerializer(settingsDump).toJson()
|
||||||
|
entry.data.put(json)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun dumpSources(): BackupEntry {
|
||||||
|
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
|
||||||
|
val all = db.getSourcesDao().findAll()
|
||||||
|
for (source in all) {
|
||||||
|
val json = JsonSerializer(source).toJson()
|
||||||
|
entry.data.put(json)
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
fun createIndex(): BackupEntry {
|
fun createIndex(): BackupEntry {
|
||||||
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
|
val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("app_id", BuildConfig.APPLICATION_ID)
|
json.put("app_id", BuildConfig.APPLICATION_ID)
|
||||||
json.put("app_version", BuildConfig.VERSION_CODE)
|
json.put("app_version", BuildConfig.VERSION_CODE)
|
||||||
@@ -77,6 +123,11 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getBackupDate(entry: BackupEntry?): Date? {
|
||||||
|
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
|
||||||
|
return if (timestamp == 0L) null else Date(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
||||||
val result = CompositeResult()
|
val result = CompositeResult()
|
||||||
for (item in entry.data.JSONIterator()) {
|
for (item in entry.data.JSONIterator()) {
|
||||||
@@ -88,9 +139,9 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
val history = JsonDeserializer(item).toHistoryEntity()
|
val history = JsonDeserializer(item).toHistoryEntity()
|
||||||
result += runCatchingCancellable {
|
result += runCatchingCancellable {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
db.getTagsDao().upsert(tags)
|
||||||
db.mangaDao.upsert(manga, tags)
|
db.getMangaDao().upsert(manga, tags)
|
||||||
db.historyDao.upsert(history)
|
db.getHistoryDao().upsert(history)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +153,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
for (item in entry.data.JSONIterator()) {
|
for (item in entry.data.JSONIterator()) {
|
||||||
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
|
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
|
||||||
result += runCatchingCancellable {
|
result += runCatchingCancellable {
|
||||||
db.favouriteCategoriesDao.upsert(category)
|
db.getFavouriteCategoriesDao().upsert(category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -119,12 +170,55 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
|
|||||||
val favourite = JsonDeserializer(item).toFavouriteEntity()
|
val favourite = JsonDeserializer(item).toFavouriteEntity()
|
||||||
result += runCatchingCancellable {
|
result += runCatchingCancellable {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
db.tagsDao.upsert(tags)
|
db.getTagsDao().upsert(tags)
|
||||||
db.mangaDao.upsert(manga, tags)
|
db.getMangaDao().upsert(manga, tags)
|
||||||
db.favouritesDao.upsert(favourite)
|
db.getFavouritesDao().upsert(favourite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun restoreBookmarks(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data.JSONIterator()) {
|
||||||
|
val mangaJson = item.getJSONObject("manga")
|
||||||
|
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||||
|
val tags = item.getJSONArray("tags").mapJSON {
|
||||||
|
JsonDeserializer(it).toTagEntity()
|
||||||
|
}
|
||||||
|
val bookmarks = item.getJSONArray("bookmarks").mapJSON {
|
||||||
|
JsonDeserializer(it).toBookmarkEntity()
|
||||||
|
}
|
||||||
|
result += runCatchingCancellable {
|
||||||
|
db.withTransaction {
|
||||||
|
db.getTagsDao().upsert(tags)
|
||||||
|
db.getMangaDao().upsert(manga, tags)
|
||||||
|
db.getBookmarksDao().upsert(bookmarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreSources(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data.JSONIterator()) {
|
||||||
|
val source = JsonDeserializer(item).toMangaSourceEntity()
|
||||||
|
result += runCatchingCancellable {
|
||||||
|
db.getSourcesDao().upsert(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreSettings(entry: BackupEntry): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
for (item in entry.data.JSONIterator()) {
|
||||||
|
result += runCatchingCancellable {
|
||||||
|
settings.upsertAll(JsonDeserializer(item).toMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,44 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okio.Closeable
|
import okio.Closeable
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
class BackupZipInput(val file: File) : Closeable {
|
class BackupZipInput(val file: File) : Closeable {
|
||||||
|
|
||||||
private val zipFile = ZipFile(file)
|
private val zipFile = ZipFile(file)
|
||||||
|
|
||||||
suspend fun getEntry(name: String): BackupEntry = runInterruptible(Dispatchers.IO) {
|
suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
|
||||||
val entry = zipFile.getEntry(name)
|
val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
|
||||||
val json = zipFile.getInputStream(entry).use {
|
val json = zipFile.getInputStream(entry).use {
|
||||||
JSONArray(it.bufferedReader().readText())
|
JSONArray(it.bufferedReader().readText())
|
||||||
}
|
}
|
||||||
BackupEntry(name, json)
|
BackupEntry(name, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
|
||||||
|
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
|
||||||
|
BackupEntry.Name.entries.find { it.key == ze.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
zipFile.close()
|
zipFile.close()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun cleanupAsync() {
|
||||||
|
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
|
||||||
|
runCatching {
|
||||||
|
close()
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okio.Closeable
|
import okio.Closeable
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.format
|
|
||||||
import org.koitharu.kotatsu.core.zip.ZipOutput
|
import org.koitharu.kotatsu.core.zip.ZipOutput
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class BackupZipOutput(val file: File) : Closeable {
|
|||||||
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
|
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
|
||||||
|
|
||||||
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
|
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
|
||||||
output.put(entry.name, entry.data.toString(2))
|
output.put(entry.name.key, entry.data.toString(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun finish() = runInterruptible(Dispatchers.IO) {
|
suspend fun finish() = runInterruptible(Dispatchers.IO) {
|
||||||
@@ -29,7 +29,7 @@ class BackupZipOutput(val file: File) : Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val DIR_BACKUPS = "backups"
|
const val DIR_BACKUPS = "backups"
|
||||||
|
|
||||||
suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
|
suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
|
||||||
val dir = context.run {
|
val dir = context.run {
|
||||||
@@ -39,7 +39,7 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl
|
|||||||
val filename = buildString {
|
val filename = buildString {
|
||||||
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
||||||
append('_')
|
append('_')
|
||||||
append(Date().format("ddMMyyyy"))
|
append(LocalDate.now().format(DateTimeFormatter.ofPattern("ddMMyyyy")))
|
||||||
append(".bk.zip")
|
append(".bk.zip")
|
||||||
}
|
}
|
||||||
BackupZipOutput(File(dir, filename))
|
BackupZipOutput(File(dir, filename))
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ class CompositeResult {
|
|||||||
val failures: List<Throwable>
|
val failures: List<Throwable>
|
||||||
get() = errors.filterNotNull()
|
get() = errors.filterNotNull()
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = errors.isEmpty() && successCount == 0
|
||||||
|
|
||||||
val isAllSuccess: Boolean
|
val isAllSuccess: Boolean
|
||||||
get() = errors.none { it != null }
|
get() = errors.none { it != null }
|
||||||
|
|
||||||
@@ -36,4 +39,4 @@ class CompositeResult {
|
|||||||
result.errors.addAll(other.errors)
|
result.errors.addAll(other.errors)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
@@ -34,14 +36,14 @@ class JsonDeserializer(private val json: JSONObject) {
|
|||||||
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
largeCoverUrl = json.getStringOrNull("large_cover_url"),
|
||||||
state = json.getStringOrNull("state"),
|
state = json.getStringOrNull("state"),
|
||||||
author = json.getStringOrNull("author"),
|
author = json.getStringOrNull("author"),
|
||||||
source = json.getString("source")
|
source = json.getString("source"),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toTagEntity() = TagEntity(
|
fun toTagEntity() = TagEntity(
|
||||||
id = json.getLong("id"),
|
id = json.getLong("id"),
|
||||||
title = json.getString("title"),
|
title = json.getString("title"),
|
||||||
key = json.getString("key"),
|
key = json.getString("key"),
|
||||||
source = json.getString("source")
|
source = json.getString("source"),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toHistoryEntity() = HistoryEntity(
|
fun toHistoryEntity() = HistoryEntity(
|
||||||
@@ -65,4 +67,34 @@ class JsonDeserializer(private val json: JSONObject) {
|
|||||||
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
|
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
|
||||||
deletedAt = 0L,
|
deletedAt = 0L,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun toBookmarkEntity() = BookmarkEntity(
|
||||||
|
mangaId = json.getLong("manga_id"),
|
||||||
|
pageId = json.getLong("page_id"),
|
||||||
|
chapterId = json.getLong("chapter_id"),
|
||||||
|
page = json.getInt("page"),
|
||||||
|
scroll = json.getInt("scroll"),
|
||||||
|
imageUrl = json.getString("image_url"),
|
||||||
|
createdAt = json.getLong("created_at"),
|
||||||
|
percent = json.getDouble("percent").toFloat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toMangaSourceEntity() = MangaSourceEntity(
|
||||||
|
source = json.getString("source"),
|
||||||
|
isEnabled = json.getBoolean("enabled"),
|
||||||
|
sortKey = json.getInt("sort_key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toMap(): Map<String, Any?> {
|
||||||
|
val map = mutableMapOf<String, Any?>()
|
||||||
|
val keys = json.keys()
|
||||||
|
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
val key = keys.next()
|
||||||
|
val value = json.get(key)
|
||||||
|
map[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||||
@@ -15,7 +17,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("category_id", e.categoryId)
|
put("category_id", e.categoryId)
|
||||||
put("sort_key", e.sortKey)
|
put("sort_key", e.sortKey)
|
||||||
put("created_at", e.createdAt)
|
put("created_at", e.createdAt)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(e: FavouriteCategoryEntity) : this(
|
constructor(e: FavouriteCategoryEntity) : this(
|
||||||
@@ -27,7 +29,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("order", e.order)
|
put("order", e.order)
|
||||||
put("track", e.track)
|
put("track", e.track)
|
||||||
put("show_in_lib", e.isVisibleInLibrary)
|
put("show_in_lib", e.isVisibleInLibrary)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(e: HistoryEntity) : this(
|
constructor(e: HistoryEntity) : this(
|
||||||
@@ -39,7 +41,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("page", e.page)
|
put("page", e.page)
|
||||||
put("scroll", e.scroll)
|
put("scroll", e.scroll)
|
||||||
put("percent", e.percent)
|
put("percent", e.percent)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(e: TagEntity) : this(
|
constructor(e: TagEntity) : this(
|
||||||
@@ -48,7 +50,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("title", e.title)
|
put("title", e.title)
|
||||||
put("key", e.key)
|
put("key", e.key)
|
||||||
put("source", e.source)
|
put("source", e.source)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(e: MangaEntity) : this(
|
constructor(e: MangaEntity) : this(
|
||||||
@@ -65,8 +67,33 @@ class JsonSerializer private constructor(private val json: JSONObject) {
|
|||||||
put("state", e.state)
|
put("state", e.state)
|
||||||
put("author", e.author)
|
put("author", e.author)
|
||||||
put("source", e.source)
|
put("source", e.source)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(e: BookmarkEntity) : this(
|
||||||
|
JSONObject().apply {
|
||||||
|
put("manga_id", e.mangaId)
|
||||||
|
put("page_id", e.pageId)
|
||||||
|
put("chapter_id", e.chapterId)
|
||||||
|
put("page", e.page)
|
||||||
|
put("scroll", e.scroll)
|
||||||
|
put("image_url", e.imageUrl)
|
||||||
|
put("created_at", e.createdAt)
|
||||||
|
put("percent", e.percent)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(e: MangaSourceEntity) : this(
|
||||||
|
JSONObject().apply {
|
||||||
|
put("source", e.source)
|
||||||
|
put("enabled", e.isEnabled)
|
||||||
|
put("sort_key", e.sortKey)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(m: Map<String, *>) : this(
|
||||||
|
JSONObject(m),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toJson(): JSONObject = json
|
fun toJson(): JSONObject = json
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ interface ContentCache {
|
|||||||
|
|
||||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
||||||
|
|
||||||
|
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
|
||||||
|
|
||||||
|
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
|
||||||
|
|
||||||
data class Key(
|
data class Key(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ExpiringLruCache<T>(
|
|||||||
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
||||||
|
|
||||||
operator fun get(key: ContentCache.Key): T? {
|
operator fun get(key: ContentCache.Key): T? {
|
||||||
val value = cache.get(key) ?: return null
|
val value = cache[key] ?: return null
|
||||||
if (value.isExpired) {
|
if (value.isExpired) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
||||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
||||||
|
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = true
|
override val isCachingEnabled: Boolean = true
|
||||||
|
|
||||||
@@ -35,6 +36,14 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
pagesCache[ContentCache.Key(source, url)] = pages
|
pagesCache[ContentCache.Key(source, url)] = pages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||||
|
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||||
|
relatedMangaCache[ContentCache.Key(source, url)] = related
|
||||||
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
|
|
||||||
override fun onLowMemory() = Unit
|
override fun onLowMemory() = Unit
|
||||||
@@ -42,6 +51,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
trimCache(detailsCache, level)
|
trimCache(detailsCache, level)
|
||||||
trimCache(pagesCache, level)
|
trimCache(pagesCache, level)
|
||||||
|
trimCache(relatedMangaCache, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
|
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
|
||||||
|
|||||||
@@ -15,4 +15,8 @@ class StubContentCache : ContentCache {
|
|||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
||||||
|
|
||||||
|
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
|
||||||
|
|
||||||
|
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import androidx.room.InvalidationTracker
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
|
||||||
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
|
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
|
||||||
import org.koitharu.kotatsu.core.db.dao.MangaDao
|
import org.koitharu.kotatsu.core.db.dao.MangaDao
|
||||||
|
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||||
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
|
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
|
||||||
import org.koitharu.kotatsu.core.db.dao.TagsDao
|
import org.koitharu.kotatsu.core.db.dao.TagsDao
|
||||||
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
|
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
|
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
|
||||||
@@ -25,6 +28,8 @@ import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
|||||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
|
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
|
||||||
|
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
|
||||||
|
import org.koitharu.kotatsu.core.db.migrations.Migration17To18
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||||
@@ -49,71 +54,74 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
|||||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||||
|
|
||||||
const val DATABASE_VERSION = 16
|
const val DATABASE_VERSION = 18
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
|
||||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
|
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
|
||||||
ScrobblingEntity::class,
|
ScrobblingEntity::class, MangaSourceEntity::class,
|
||||||
],
|
],
|
||||||
version = DATABASE_VERSION,
|
version = DATABASE_VERSION,
|
||||||
)
|
)
|
||||||
abstract class MangaDatabase : RoomDatabase() {
|
abstract class MangaDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract val historyDao: HistoryDao
|
abstract fun getHistoryDao(): HistoryDao
|
||||||
|
|
||||||
abstract val tagsDao: TagsDao
|
abstract fun getTagsDao(): TagsDao
|
||||||
|
|
||||||
abstract val mangaDao: MangaDao
|
abstract fun getMangaDao(): MangaDao
|
||||||
|
|
||||||
abstract val favouritesDao: FavouritesDao
|
abstract fun getFavouritesDao(): FavouritesDao
|
||||||
|
|
||||||
abstract val preferencesDao: PreferencesDao
|
abstract fun getPreferencesDao(): PreferencesDao
|
||||||
|
|
||||||
abstract val favouriteCategoriesDao: FavouriteCategoriesDao
|
abstract fun getFavouriteCategoriesDao(): FavouriteCategoriesDao
|
||||||
|
|
||||||
abstract val tracksDao: TracksDao
|
abstract fun getTracksDao(): TracksDao
|
||||||
|
|
||||||
abstract val trackLogsDao: TrackLogsDao
|
abstract fun getTrackLogsDao(): TrackLogsDao
|
||||||
|
|
||||||
abstract val suggestionDao: SuggestionDao
|
abstract fun getSuggestionDao(): SuggestionDao
|
||||||
|
|
||||||
abstract val bookmarksDao: BookmarksDao
|
abstract fun getBookmarksDao(): BookmarksDao
|
||||||
|
|
||||||
abstract val scrobblingDao: ScrobblingDao
|
abstract fun getScrobblingDao(): ScrobblingDao
|
||||||
|
|
||||||
|
abstract fun getSourcesDao(): MangaSourcesDao
|
||||||
}
|
}
|
||||||
|
|
||||||
val databaseMigrations: Array<Migration>
|
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||||
get() = arrayOf(
|
Migration1To2(),
|
||||||
Migration1To2(),
|
Migration2To3(),
|
||||||
Migration2To3(),
|
Migration3To4(),
|
||||||
Migration3To4(),
|
Migration4To5(),
|
||||||
Migration4To5(),
|
Migration5To6(),
|
||||||
Migration5To6(),
|
Migration6To7(),
|
||||||
Migration6To7(),
|
Migration7To8(),
|
||||||
Migration7To8(),
|
Migration8To9(),
|
||||||
Migration8To9(),
|
Migration9To10(),
|
||||||
Migration9To10(),
|
Migration10To11(),
|
||||||
Migration10To11(),
|
Migration11To12(),
|
||||||
Migration11To12(),
|
Migration12To13(),
|
||||||
Migration12To13(),
|
Migration13To14(),
|
||||||
Migration13To14(),
|
Migration14To15(),
|
||||||
Migration14To15(),
|
Migration15To16(),
|
||||||
Migration15To16(),
|
Migration16To17(context),
|
||||||
)
|
Migration17To18(),
|
||||||
|
)
|
||||||
|
|
||||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||||
.databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db")
|
.databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db")
|
||||||
.addMigrations(*databaseMigrations)
|
.addMigrations(*getDatabaseMigrations(context))
|
||||||
.addCallback(DatabasePrePopulateCallback(context.resources))
|
.addCallback(DatabasePrePopulateCallback(context.resources))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
|
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
|
||||||
val scope = processLifecycleScope
|
val scope = processLifecycleScope
|
||||||
if (scope.isActive) {
|
if (scope.isActive) {
|
||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
|
||||||
removeObserver(observer)
|
removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ const val TABLE_TAGS = "tags"
|
|||||||
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||||
const val TABLE_HISTORY = "history"
|
const val TABLE_HISTORY = "history"
|
||||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||||
|
const val TABLE_SOURCES = "sources"
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import androidx.room.Upsert
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||||
@@ -13,6 +20,14 @@ abstract class MangaDao {
|
|||||||
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
@Query("SELECT * FROM manga WHERE manga_id = :id")
|
||||||
abstract suspend fun find(id: Long): MangaWithTags?
|
abstract suspend fun find(id: Long): MangaWithTags?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
|
||||||
|
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM manga WHERE source = :source")
|
||||||
|
abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||||
@@ -21,8 +36,8 @@ abstract class MangaDao {
|
|||||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||||
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Upsert
|
||||||
abstract suspend fun insert(manga: MangaEntity): Long
|
abstract suspend fun upsert(manga: MangaEntity)
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
abstract suspend fun update(manga: MangaEntity): Int
|
abstract suspend fun update(manga: MangaEntity): Int
|
||||||
@@ -33,17 +48,19 @@ abstract class MangaDao {
|
|||||||
@Query("DELETE FROM manga_tags WHERE manga_id = :mangaId")
|
@Query("DELETE FROM manga_tags WHERE manga_id = :mangaId")
|
||||||
abstract suspend fun clearTagRelation(mangaId: Long)
|
abstract suspend fun clearTagRelation(mangaId: Long)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Delete
|
||||||
|
abstract suspend fun delete(subjects: Collection<MangaEntity>)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
|
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
|
||||||
if (update(manga) <= 0) {
|
upsert(manga)
|
||||||
insert(manga)
|
if (tags != null) {
|
||||||
if (tags != null) {
|
clearTagRelation(manga.id)
|
||||||
clearTagRelation(manga.id)
|
tags.map {
|
||||||
tags.map {
|
MangaTagsEntity(manga.id, it.id)
|
||||||
MangaTagsEntity(manga.id, it.id)
|
}.forEach {
|
||||||
}.forEach {
|
insertTagRelation(it)
|
||||||
insertTagRelation(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RawQuery
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||||
|
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class MangaSourcesDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
|
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
||||||
|
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources WHERE enabled = 0")
|
||||||
|
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
|
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT IFNULL(MAX(sort_key),0) FROM sources")
|
||||||
|
abstract suspend fun getMaxSortKey(): Int
|
||||||
|
|
||||||
|
@Query("UPDATE sources SET enabled = 0")
|
||||||
|
abstract suspend fun disableAllSources()
|
||||||
|
|
||||||
|
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
|
||||||
|
abstract suspend fun setSortKey(source: String, sortKey: Int)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
@Transaction
|
||||||
|
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
abstract suspend fun upsert(entry: MangaSourceEntity)
|
||||||
|
|
||||||
|
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
|
||||||
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
|
@Language("RoomSql")
|
||||||
|
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
|
||||||
|
return observeImpl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
|
||||||
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
|
@Language("RoomSql")
|
||||||
|
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
|
||||||
|
return findAllImpl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
|
||||||
|
if (updateIsEnabled(source, isEnabled) == 0) {
|
||||||
|
val entity = MangaSourceEntity(
|
||||||
|
source = source,
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
sortKey = getMaxSortKey() + 1,
|
||||||
|
)
|
||||||
|
upsert(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
|
||||||
|
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int
|
||||||
|
|
||||||
|
@RawQuery(observedEntities = [MangaSourceEntity::class])
|
||||||
|
protected abstract fun observeImpl(query: SupportSQLiteQuery): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
@RawQuery
|
||||||
|
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
|
||||||
|
|
||||||
|
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
|
||||||
|
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
||||||
|
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
||||||
|
SourcesSortOrder.MANUAL -> "sort_key ASC"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.core.db.dao
|
package org.koitharu.kotatsu.core.db.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -12,6 +14,7 @@ abstract class TagsDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""SELECT tags.* FROM tags
|
"""SELECT tags.* FROM tags
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit""",
|
LIMIT :limit""",
|
||||||
@@ -21,7 +24,7 @@ abstract class TagsDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""SELECT tags.* FROM tags
|
"""SELECT tags.* FROM tags
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
WHERE tags.source = :source
|
WHERE tags.source = :source
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit""",
|
LIMIT :limit""",
|
||||||
@@ -31,7 +34,17 @@ abstract class TagsDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""SELECT tags.* FROM tags
|
"""SELECT tags.* FROM tags
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
WHERE tags.source = :source AND title LIKE :query
|
WHERE tags.source = :source
|
||||||
|
GROUP BY tags.title
|
||||||
|
ORDER BY COUNT(manga_id) ASC
|
||||||
|
LIMIT :limit""",
|
||||||
|
)
|
||||||
|
abstract suspend fun findRareTags(source: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE tags.source = :source AND title LIKE :query
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit""",
|
LIMIT :limit""",
|
||||||
@@ -41,13 +54,35 @@ abstract class TagsDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""SELECT tags.* FROM tags
|
"""SELECT tags.* FROM tags
|
||||||
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
|
||||||
WHERE title LIKE :query
|
WHERE title LIKE :query AND manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
|
||||||
GROUP BY tags.title
|
GROUP BY tags.title
|
||||||
ORDER BY COUNT(manga_id) DESC
|
ORDER BY COUNT(manga_id) DESC
|
||||||
LIMIT :limit""",
|
LIMIT :limit""",
|
||||||
)
|
)
|
||||||
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
abstract suspend fun findTags(query: String, limit: Int): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT tags.* FROM manga_tags
|
||||||
|
LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE manga_tags.manga_id IN (SELECT manga_id FROM manga_tags WHERE tag_id = :tagId)
|
||||||
|
GROUP BY tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC;
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
abstract suspend fun findRelatedTags(tagId: Long): List<TagEntity>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT tags.* FROM manga_tags
|
||||||
|
LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id
|
||||||
|
WHERE manga_tags.manga_id IN (SELECT manga_id FROM manga_tags WHERE tag_id IN (:ids))
|
||||||
|
GROUP BY tags.tag_id
|
||||||
|
ORDER BY COUNT(manga_id) DESC;
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
abstract suspend fun findRelatedTags(ids: Set<Long>): List<TagEntity>
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(tags: Iterable<TagEntity>)
|
abstract suspend fun upsert(tags: Iterable<TagEntity>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ fun TagEntity.toMangaTag() = MangaTag(
|
|||||||
|
|
||||||
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
|
||||||
|
|
||||||
|
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
|
||||||
|
|
||||||
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
title = this.title,
|
title = this.title,
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ data class MangaPrefsEntity(
|
|||||||
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
||||||
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
|
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
|
||||||
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
|
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
|
||||||
|
@ColumnInfo(name = "cf_grayscale") val cfGrayscale: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = TABLE_SOURCES,
|
||||||
|
)
|
||||||
|
data class MangaSourceEntity(
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
@ColumnInfo(name = "source")
|
||||||
|
val source: String,
|
||||||
|
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||||
|
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ import androidx.room.Embedded
|
|||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
|
||||||
class MangaWithTags(
|
data class MangaWithTags(
|
||||||
@Embedded val manga: MangaEntity,
|
@Embedded val manga: MangaEntity,
|
||||||
@Relation(
|
@Relation(
|
||||||
parentColumn = "manga_id",
|
parentColumn = "manga_id",
|
||||||
@@ -12,23 +12,4 @@ class MangaWithTags(
|
|||||||
associateBy = Junction(MangaTagsEntity::class)
|
associateBy = Junction(MangaTagsEntity::class)
|
||||||
)
|
)
|
||||||
val tags: List<TagEntity>,
|
val tags: List<TagEntity>,
|
||||||
) {
|
)
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as MangaWithTags
|
|
||||||
|
|
||||||
if (manga != other.manga) return false
|
|
||||||
if (tags != other.tags) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = manga.hashCode()
|
|
||||||
result = 31 * result + tags.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration10To11 : Migration(10, 11) {
|
class Migration10To11 : Migration(10, 11) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `bookmarks` (
|
CREATE TABLE IF NOT EXISTS `bookmarks` (
|
||||||
`manga_id` INTEGER NOT NULL,
|
`manga_id` INTEGER NOT NULL,
|
||||||
@@ -20,7 +20,7 @@ class Migration10To11 : Migration(10, 11) {
|
|||||||
FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )
|
FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)")
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration11To12 : Migration(11, 12) {
|
class Migration11To12 : Migration(11, 12) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `scrobblings` (
|
CREATE TABLE IF NOT EXISTS `scrobblings` (
|
||||||
`scrobbler` INTEGER NOT NULL,
|
`scrobbler` INTEGER NOT NULL,
|
||||||
@@ -21,7 +21,7 @@ class Migration11To12 : Migration(11, 12) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL("ALTER TABLE history ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
|
db.execSQL("ALTER TABLE history ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
|
||||||
database.execSQL("ALTER TABLE bookmarks ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
|
db.execSQL("ALTER TABLE bookmarks ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration12To13 : Migration(12, 13) {
|
class Migration12To13 : Migration(12, 13) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1")
|
db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1")
|
||||||
database.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration13To14 : Migration(13, 14) {
|
class Migration13To14 : Migration(13, 14) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
||||||
database.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
||||||
database.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
|
||||||
|
db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,5 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration14To15 : Migration(14, 15) {
|
class Migration14To15 : Migration(14, 15) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) = Unit
|
||||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
|
|
||||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration15To16 : Migration(15, 16) {
|
class Migration15To16 : Migration(15, 16) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class Migration16To17(context: Context) : Migration(16, 17) {
|
||||||
|
|
||||||
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("CREATE TABLE `sources` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))")
|
||||||
|
db.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
|
||||||
|
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
|
||||||
|
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
|
||||||
|
val sources = MangaSource.entries
|
||||||
|
for (source in sources) {
|
||||||
|
if (source == MangaSource.LOCAL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val name = source.name
|
||||||
|
val isHidden = name in hiddenSources
|
||||||
|
var sortKey = order.indexOf(name)
|
||||||
|
if (sortKey == -1) {
|
||||||
|
if (isHidden) {
|
||||||
|
sortKey = order.size + source.ordinal
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.execSQL(
|
||||||
|
"INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)",
|
||||||
|
arrayOf(name, (!isHidden).toInt(), sortKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Boolean.toInt() = if (this) 1 else 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
class Migration17To18 : Migration(17, 18) {
|
||||||
|
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_grayscale` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,48 +7,48 @@ class Migration1To2 : Migration(1, 2) {
|
|||||||
/**
|
/**
|
||||||
* Adding foreign keys
|
* Adding foreign keys
|
||||||
*/
|
*/
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
/* manga_tags */
|
/* manga_tags */
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
|
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
|
||||||
"PRIMARY KEY(manga_id, tag_id), " +
|
"PRIMARY KEY(manga_id, tag_id), " +
|
||||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
|
||||||
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
)
|
)
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
|
||||||
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
|
db.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
|
||||||
database.execSQL("DROP TABLE manga_tags")
|
db.execSQL("DROP TABLE manga_tags")
|
||||||
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
|
db.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
|
||||||
/* favourites */
|
/* favourites */
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
|
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
|
||||||
"PRIMARY KEY(manga_id, category_id), " +
|
"PRIMARY KEY(manga_id, category_id), " +
|
||||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
|
||||||
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
)
|
)
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
|
||||||
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
|
db.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
|
||||||
database.execSQL("DROP TABLE favourites")
|
db.execSQL("DROP TABLE favourites")
|
||||||
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
|
db.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
|
||||||
/* history */
|
/* history */
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
|
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
|
||||||
"PRIMARY KEY(manga_id), " +
|
"PRIMARY KEY(manga_id), " +
|
||||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
)
|
)
|
||||||
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
|
db.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
|
||||||
database.execSQL("DROP TABLE history")
|
db.execSQL("DROP TABLE history")
|
||||||
database.execSQL("ALTER TABLE history_tmp RENAME TO history")
|
db.execSQL("ALTER TABLE history_tmp RENAME TO history")
|
||||||
/* preferences */
|
/* preferences */
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
|
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
|
||||||
" PRIMARY KEY(manga_id), " +
|
" PRIMARY KEY(manga_id), " +
|
||||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||||
)
|
)
|
||||||
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
|
db.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
|
||||||
database.execSQL("DROP TABLE preferences")
|
db.execSQL("DROP TABLE preferences")
|
||||||
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
|
db.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration2To3 : Migration(2, 3) {
|
class Migration2To3 : Migration(2, 3) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration3To4 : Migration(3, 4) {
|
class Migration3To4 : Migration(3, 4) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
db.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration4To5 : Migration(4, 5) {
|
class Migration4To5 : Migration(4, 5) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration5To6 : Migration(5, 6) {
|
class Migration5To6 : Migration(5, 6) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
db.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration6To7 : Migration(6, 7) {
|
class Migration6To7 : Migration(6, 7) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
|
db.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration7To8 : Migration(7, 8) {
|
class Migration7To8 : Migration(7, 8) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
|
db.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
db.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
|
db.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
|
|
||||||
class Migration8To9 : Migration(8, 9) {
|
class Migration8To9 : Migration(8, 9) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
|
db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
class Migration9To10 : Migration(9, 10) {
|
class Migration9To10 : Migration(9, 10) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1")
|
db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.exceptions
|
|||||||
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
class CloudFlareProtectedException(
|
||||||
val url: String,
|
val url: String,
|
||||||
|
val source: MangaSource?,
|
||||||
@Transient val headers: Headers,
|
@Transient val headers: Headers,
|
||||||
) : IOException("Protected by CloudFlare")
|
) : IOException("Protected by CloudFlare")
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.exceptions
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
|
||||||
|
|
||||||
class CompositeException(val errors: Collection<Throwable>) : Exception() {
|
|
||||||
|
|
||||||
override val message: String = errors.mapNotNullToSet { it.message }.joinToString()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import okio.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
class TooManyRequestExceptions(
|
||||||
|
val url: String,
|
||||||
|
val retryAt: Instant?,
|
||||||
|
) : IOException() {
|
||||||
|
val retryAfter: Long
|
||||||
|
get() = retryAt?.until(Instant.now(), ChronoUnit.MILLIS) ?: 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.koitharu.kotatsu.core.fs
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import org.koitharu.kotatsu.core.util.iterator.CloseableIterator
|
||||||
|
import org.koitharu.kotatsu.core.util.iterator.MappingIterator
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class FileSequence(private val dir: File) : Sequence<File> {
|
||||||
|
|
||||||
|
override fun iterator(): Iterator<File> {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val stream = Files.newDirectoryStream(dir.toPath())
|
||||||
|
CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream)
|
||||||
|
} else {
|
||||||
|
dir.listFiles().orEmpty().iterator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.github
|
|||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class VersionId(
|
data class VersionId(
|
||||||
val major: Int,
|
val major: Int,
|
||||||
val minor: Int,
|
val minor: Int,
|
||||||
val build: Int,
|
val build: Int,
|
||||||
@@ -30,30 +30,6 @@ class VersionId(
|
|||||||
return variantNumber.compareTo(other.variantNumber)
|
return variantNumber.compareTo(other.variantNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as VersionId
|
|
||||||
|
|
||||||
if (major != other.major) return false
|
|
||||||
if (minor != other.minor) return false
|
|
||||||
if (build != other.build) return false
|
|
||||||
if (variantType != other.variantType) return false
|
|
||||||
if (variantNumber != other.variantNumber) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = major
|
|
||||||
result = 31 * result + minor
|
|
||||||
result = 31 * result + build
|
|
||||||
result = 31 * result + variantType.hashCode()
|
|
||||||
result = 31 * result + variantNumber
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
|
private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
|
||||||
"a", "alpha" -> 1
|
"a", "alpha" -> 1
|
||||||
"b", "beta" -> 2
|
"b", "beta" -> 2
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.time.LocalDateTime
|
||||||
import java.util.Date
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
@@ -41,11 +42,7 @@ class FileLogger(
|
|||||||
}
|
}
|
||||||
val isEnabled: Boolean
|
val isEnabled: Boolean
|
||||||
get() = settings.isLoggingEnabled
|
get() = settings.isLoggingEnabled
|
||||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance(
|
private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(Locale.ROOT)
|
||||||
SimpleDateFormat.SHORT,
|
|
||||||
SimpleDateFormat.SHORT,
|
|
||||||
Locale.ROOT,
|
|
||||||
)
|
|
||||||
private val buffer = ConcurrentLinkedQueue<String>()
|
private val buffer = ConcurrentLinkedQueue<String>()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var flushJob: Job? = null
|
private var flushJob: Job? = null
|
||||||
@@ -55,7 +52,7 @@ class FileLogger(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val text = buildString {
|
val text = buildString {
|
||||||
append(dateFormat.format(Date()))
|
append(dateTimeFormatter.format(LocalDateTime.now()))
|
||||||
append(": ")
|
append(": ")
|
||||||
if (e != null) {
|
if (e != null) {
|
||||||
append("E!")
|
append("E!")
|
||||||
|
|||||||
@@ -2,16 +2,34 @@ package org.koitharu.kotatsu.core.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
import java.util.*
|
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class FavouriteCategory(
|
data class FavouriteCategory(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val sortKey: Int,
|
val sortKey: Int,
|
||||||
val order: SortOrder,
|
val order: ListSortOrder,
|
||||||
val createdAt: Date,
|
val createdAt: Instant,
|
||||||
val isTrackingEnabled: Boolean,
|
val isTrackingEnabled: Boolean,
|
||||||
val isVisibleInLibrary: Boolean,
|
val isVisibleInLibrary: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable, ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is FavouriteCategory && id == other.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(previousState: ListModel): Any? {
|
||||||
|
if (previousState !is FavouriteCategory) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return if (isTrackingEnabled != previousState.isTrackingEnabled || isVisibleInLibrary != previousState.isVisibleInLibrary) {
|
||||||
|
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.iterator
|
import org.koitharu.kotatsu.core.util.ext.iterator
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@JvmName("mangaIds")
|
@JvmName("mangaIds")
|
||||||
fun Collection<Manga>.ids() = mapToSet { it.id }
|
fun Collection<Manga>.ids() = mapToSet { it.id }
|
||||||
@@ -16,6 +23,8 @@ fun Collection<Manga>.distinctById() = distinctBy { it.id }
|
|||||||
@JvmName("chaptersIds")
|
@JvmName("chaptersIds")
|
||||||
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
|
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
|
||||||
|
|
||||||
|
fun Collection<MangaChapter>.findById(id: Long) = find { x -> x.id == id }
|
||||||
|
|
||||||
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
|
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
|
||||||
if (size <= 1) {
|
if (size <= 1) {
|
||||||
return size
|
return size
|
||||||
@@ -28,8 +37,36 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
|
|||||||
return acc.values.max()
|
return acc.values.max()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val MangaState.titleResId: Int
|
||||||
|
get() = when (this) {
|
||||||
|
MangaState.ONGOING -> R.string.state_ongoing
|
||||||
|
MangaState.FINISHED -> R.string.state_finished
|
||||||
|
MangaState.ABANDONED -> R.string.state_abandoned
|
||||||
|
MangaState.PAUSED -> R.string.state_paused
|
||||||
|
MangaState.UPCOMING -> R.string.state_upcoming
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val MangaState.iconResId: Int
|
||||||
|
get() = when (this) {
|
||||||
|
MangaState.ONGOING -> R.drawable.ic_play
|
||||||
|
MangaState.FINISHED -> R.drawable.ic_state_finished
|
||||||
|
MangaState.ABANDONED -> R.drawable.ic_state_abandoned
|
||||||
|
MangaState.PAUSED -> R.drawable.ic_action_pause
|
||||||
|
MangaState.UPCOMING -> materialR.drawable.ic_clock_black_24dp
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val ContentRating.titleResId: Int
|
||||||
|
get() = when (this) {
|
||||||
|
ContentRating.SAFE -> R.string.rating_safe
|
||||||
|
ContentRating.SUGGESTIVE -> R.string.rating_suggestive
|
||||||
|
ContentRating.ADULT -> R.string.rating_adult
|
||||||
|
}
|
||||||
|
|
||||||
fun Manga.findChapter(id: Long): MangaChapter? {
|
fun Manga.findChapter(id: Long): MangaChapter? {
|
||||||
return chapters?.find { it.id == id }
|
return chapters?.findById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||||
@@ -38,7 +75,7 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (history != null) {
|
if (history != null) {
|
||||||
val currentChapter = ch.find { it.id == history.chapterId }
|
val currentChapter = ch.findById(history.chapterId)
|
||||||
if (currentChapter != null) {
|
if (currentChapter != null) {
|
||||||
return currentChapter.branch
|
return currentChapter.branch
|
||||||
}
|
}
|
||||||
@@ -47,10 +84,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
|||||||
if (groups.size == 1) {
|
if (groups.size == 1) {
|
||||||
return groups.keys.first()
|
return groups.keys.first()
|
||||||
}
|
}
|
||||||
val candidates = HashMap<String?, List<MangaChapter>>(groups.size)
|
|
||||||
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
||||||
val displayLanguage = locale.getDisplayLanguage(locale)
|
val displayLanguage = locale.getDisplayLanguage(locale)
|
||||||
val displayName = locale.getDisplayName(locale)
|
val displayName = locale.getDisplayName(locale)
|
||||||
|
val candidates = HashMap<String?, List<MangaChapter>>(3)
|
||||||
for (branch in groups.keys) {
|
for (branch in groups.keys) {
|
||||||
if (branch != null && (
|
if (branch != null && (
|
||||||
branch.contains(displayLanguage, ignoreCase = true) ||
|
branch.contains(displayLanguage, ignoreCase = true) ||
|
||||||
@@ -60,9 +97,19 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
|||||||
candidates[branch] = groups[branch] ?: continue
|
candidates[branch] = groups[branch] ?: continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (candidates.isNotEmpty()) {
|
||||||
|
return candidates.maxBy { it.value.size }.key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key
|
return groups.maxByOrNull { it.value.size }?.key
|
||||||
}
|
}
|
||||||
|
|
||||||
val Manga.isLocal: Boolean
|
val Manga.isLocal: Boolean
|
||||||
get() = source == MangaSource.LOCAL
|
get() = source == MangaSource.LOCAL
|
||||||
|
|
||||||
|
val Manga.appUrl: Uri
|
||||||
|
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
|
||||||
|
.appendQueryParameter("source", source.name)
|
||||||
|
.appendQueryParameter("name", title)
|
||||||
|
.appendQueryParameter("url", url)
|
||||||
|
.build()
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package org.koitharu.kotatsu.core.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.*
|
import java.time.Instant
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MangaHistory(
|
data class MangaHistory(
|
||||||
val createdAt: Date,
|
val createdAt: Instant,
|
||||||
val updatedAt: Date,
|
val updatedAt: Instant,
|
||||||
val chapterId: Long,
|
val chapterId: Long,
|
||||||
val page: Int,
|
val page: Int,
|
||||||
val scroll: Int,
|
val scroll: Int,
|
||||||
val percent: Float,
|
val percent: Float,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
@@ -1,17 +1,62 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.RelativeSizeSpan
|
||||||
|
import android.text.style.SuperscriptSpan
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.inSpans
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
fun MangaSource.getLocaleTitle(): String? {
|
|
||||||
val lc = Locale(locale ?: return null)
|
|
||||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MangaSource(name: String): MangaSource {
|
fun MangaSource(name: String): MangaSource {
|
||||||
MangaSource.values().forEach {
|
MangaSource.entries.forEach {
|
||||||
if (it.name == name) return it
|
if (it.name == name) return it
|
||||||
}
|
}
|
||||||
return MangaSource.DUMMY
|
return MangaSource.DUMMY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val ContentType.titleResId
|
||||||
|
get() = when (this) {
|
||||||
|
ContentType.MANGA -> R.string.content_type_manga
|
||||||
|
ContentType.HENTAI -> R.string.content_type_hentai
|
||||||
|
ContentType.COMICS -> R.string.content_type_comics
|
||||||
|
ContentType.OTHER -> R.string.content_type_other
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource.getSummary(context: Context): String {
|
||||||
|
val type = context.getString(contentType.titleResId)
|
||||||
|
val locale = locale?.toLocale().getDisplayName(context)
|
||||||
|
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource.getTitle(context: Context): CharSequence = if (isNsfw()) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||||
|
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
|
||||||
|
RelativeSizeSpan(0.74f),
|
||||||
|
SuperscriptSpan(),
|
||||||
|
) {
|
||||||
|
append(context.getString(R.string.nsfw))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.model.parcelable
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import androidx.core.os.ParcelCompat
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
|
||||||
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.MangaTag
|
|
||||||
|
|
||||||
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(readParcelableCompat<ParcelableMangaTags>()).tags,
|
|
||||||
state = readSerializableCompat(),
|
|
||||||
author = readString(),
|
|
||||||
chapters = readParcelableCompat<ParcelableMangaChapters>()?.chapters,
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun MangaPage.writeToParcel(out: Parcel) {
|
|
||||||
out.writeLong(id)
|
|
||||||
out.writeString(url)
|
|
||||||
out.writeString(preview)
|
|
||||||
out.writeSerializable(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Parcel.readMangaPage() = MangaPage(
|
|
||||||
id = readLong(),
|
|
||||||
url = requireNotNull(readString()),
|
|
||||||
preview = readString(),
|
|
||||||
source = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = checkNotNull(readSerializableCompat()),
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model.parcelable
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableChapter(
|
||||||
|
val chapter: MangaChapter,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
companion object : Parceler<ParcelableChapter> {
|
||||||
|
|
||||||
|
override fun create(parcel: Parcel) = ParcelableChapter(
|
||||||
|
MangaChapter(
|
||||||
|
id = parcel.readLong(),
|
||||||
|
name = parcel.readString().orEmpty(),
|
||||||
|
number = parcel.readInt(),
|
||||||
|
url = parcel.readString().orEmpty(),
|
||||||
|
scanlator = parcel.readString(),
|
||||||
|
uploadDate = parcel.readLong(),
|
||||||
|
branch = parcel.readString(),
|
||||||
|
source = parcel.readSerializableCompat() ?: MangaSource.DUMMY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
|
||||||
|
parcel.writeLong(id)
|
||||||
|
parcel.writeString(name)
|
||||||
|
parcel.writeInt(number)
|
||||||
|
parcel.writeString(url)
|
||||||
|
parcel.writeString(scanlator)
|
||||||
|
parcel.writeLong(uploadDate)
|
||||||
|
parcel.writeString(branch)
|
||||||
|
parcel.writeSerializable(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,56 +2,55 @@ package org.koitharu.kotatsu.core.model.parcelable
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.core.os.ParcelCompat
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
// Limits to avoid TransactionTooLargeException
|
@Parcelize
|
||||||
private const val MAX_SAFE_SIZE = 1024 * 100 // Assume that 100 kb is safe parcel size
|
data class ParcelableManga(
|
||||||
private const val MAX_SAFE_CHAPTERS_COUNT = 24 // this is 100% safe
|
|
||||||
|
|
||||||
class ParcelableManga(
|
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
private val withChapters: Boolean,
|
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(parcel.readManga(), true)
|
companion object : Parceler<ParcelableManga> {
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) {
|
||||||
val chapters = manga.chapters
|
parcel.writeLong(id)
|
||||||
if (!withChapters || chapters == null) {
|
parcel.writeString(title)
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
parcel.writeString(altTitle)
|
||||||
return
|
parcel.writeString(url)
|
||||||
}
|
parcel.writeString(publicUrl)
|
||||||
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
|
parcel.writeFloat(rating)
|
||||||
// fast path
|
ParcelCompat.writeBoolean(parcel, isNsfw)
|
||||||
manga.writeToParcel(parcel, flags, withChapters = true)
|
parcel.writeString(coverUrl)
|
||||||
return
|
parcel.writeString(largeCoverUrl)
|
||||||
}
|
parcel.writeString(description)
|
||||||
val tempParcel = Parcel.obtain()
|
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
||||||
manga.writeToParcel(tempParcel, flags, withChapters = true)
|
parcel.writeSerializable(state)
|
||||||
val size = tempParcel.dataSize()
|
parcel.writeString(author)
|
||||||
if (size < MAX_SAFE_SIZE) {
|
parcel.writeSerializable(source)
|
||||||
parcel.appendFrom(tempParcel, 0, size)
|
|
||||||
} else {
|
|
||||||
manga.writeToParcel(parcel, flags, withChapters = false)
|
|
||||||
}
|
|
||||||
tempParcel.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "ParcelableManga(manga=$manga, withChapters=$withChapters)"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): ParcelableManga {
|
|
||||||
return ParcelableManga(parcel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<ParcelableManga?> {
|
override fun create(parcel: Parcel) = ParcelableManga(
|
||||||
return arrayOfNulls(size)
|
Manga(
|
||||||
}
|
id = parcel.readLong(),
|
||||||
|
title = requireNotNull(parcel.readString()),
|
||||||
|
altTitle = parcel.readString(),
|
||||||
|
url = requireNotNull(parcel.readString()),
|
||||||
|
publicUrl = requireNotNull(parcel.readString()),
|
||||||
|
rating = parcel.readFloat(),
|
||||||
|
isNsfw = ParcelCompat.readBoolean(parcel),
|
||||||
|
coverUrl = requireNotNull(parcel.readString()),
|
||||||
|
largeCoverUrl = parcel.readString(),
|
||||||
|
description = parcel.readString(),
|
||||||
|
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
|
||||||
|
state = parcel.readSerializableCompat(),
|
||||||
|
author = parcel.readString(),
|
||||||
|
chapters = null,
|
||||||
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "ParcelableMangaChapters(chapters=$chapters)"
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model.parcelable
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.TypeParceler
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
|
||||||
|
object MangaPageParceler : Parceler<MangaPage> {
|
||||||
|
override fun create(parcel: Parcel) = MangaPage(
|
||||||
|
id = parcel.readLong(),
|
||||||
|
url = requireNotNull(parcel.readString()),
|
||||||
|
preview = parcel.readString(),
|
||||||
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun MangaPage.write(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeLong(id)
|
||||||
|
parcel.writeString(url)
|
||||||
|
parcel.writeString(preview)
|
||||||
|
parcel.writeSerializable(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@TypeParceler<MangaPage, MangaPageParceler>
|
||||||
|
class ParcelableMangaPage(val page: MangaPage) : Parcelable
|
||||||
@@ -1,39 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "ParcelableMangaPages(pages=$pages)"
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,39 +2,26 @@ package org.koitharu.kotatsu.core.model.parcelable
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import org.koitharu.kotatsu.core.util.ext.Set
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.TypeParceler
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
class ParcelableMangaTags(
|
object MangaTagParceler : Parceler<MangaTag> {
|
||||||
val tags: Set<MangaTag>,
|
override fun create(parcel: Parcel) = MangaTag(
|
||||||
) : Parcelable {
|
title = requireNotNull(parcel.readString()),
|
||||||
|
key = requireNotNull(parcel.readString()),
|
||||||
constructor(parcel: Parcel) : this(
|
source = requireNotNull(parcel.readSerializableCompat()),
|
||||||
Set(parcel.readInt()) { parcel.readMangaTag() },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun MangaTag.write(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeInt(tags.size)
|
parcel.writeString(title)
|
||||||
for (tag in tags) {
|
parcel.writeString(key)
|
||||||
tag.writeToParcel(parcel)
|
parcel.writeSerializable(source)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "ParcelableMangaTags(tags=$tags)"
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@TypeParceler<MangaTag, MangaTagParceler>
|
||||||
|
data class ParcelableMangaTags(val tags: Set<MangaTag>) : Parcelable
|
||||||
|
|||||||
@@ -3,23 +3,26 @@ package org.koitharu.kotatsu.core.network
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
|
import org.jsoup.Jsoup
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
import java.net.HttpURLConnection.HTTP_FORBIDDEN
|
||||||
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||||
|
|
||||||
private const val HEADER_SERVER = "Server"
|
|
||||||
private const val SERVER_CLOUDFLARE = "cloudflare"
|
|
||||||
|
|
||||||
class CloudFlareInterceptor : Interceptor {
|
class CloudFlareInterceptor : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val response = chain.proceed(chain.request())
|
val response = chain.proceed(chain.request())
|
||||||
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
||||||
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
|
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
|
||||||
|
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
|
||||||
|
} ?: return response
|
||||||
|
if (content.getElementById("challenge-error-title") != null) {
|
||||||
val request = response.request
|
val request = response.request
|
||||||
response.closeQuietly()
|
response.closeQuietly()
|
||||||
throw CloudFlareProtectedException(
|
throw CloudFlareProtectedException(
|
||||||
url = request.url.toString(),
|
url = request.url.toString(),
|
||||||
|
source = request.tag(MangaSource::class.java),
|
||||||
headers = request.headers,
|
headers = request.headers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ object CommonHeaders {
|
|||||||
const val AUTHORIZATION = "Authorization"
|
const val AUTHORIZATION = "Authorization"
|
||||||
const val CACHE_CONTROL = "Cache-Control"
|
const val CACHE_CONTROL = "Cache-Control"
|
||||||
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
|
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
|
||||||
|
const val RETRY_AFTER = "Retry-After"
|
||||||
|
|
||||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||||
get() = CacheControl.Builder().noStore().build()
|
get() = CacheControl.Builder().noStore().build()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
|
|||||||
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
|
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
|
||||||
|
|
||||||
class GZipInterceptor : Interceptor {
|
class GZipInterceptor : Interceptor {
|
||||||
@@ -9,6 +10,10 @@ class GZipInterceptor : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val newRequest = chain.request().newBuilder()
|
val newRequest = chain.request().newBuilder()
|
||||||
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
newRequest.addHeader(CONTENT_ENCODING, "gzip")
|
||||||
return chain.proceed(newRequest.build())
|
return try {
|
||||||
|
chain.proceed(newRequest.build())
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user