Compare commits
732 Commits
Author | SHA1 | Date | |
---|---|---|---|
c1064b1ba7 | |||
3a9f59b7a5 | |||
ccbe240846 | |||
2c0d96917f | |||
0316cf47ed | |||
e4c8de1145 | |||
9c978c608d | |||
b4a88926ed | |||
de593f458f | |||
f3c8b37928 | |||
91f22c03c0 | |||
81ee1ce39a | |||
2ed54eed73 | |||
a9ef4bef8e | |||
c9b988aca6 | |||
b803dbe3af | |||
35ef07d720 | |||
4f596d68b9 | |||
26776f8430 | |||
471eb36a92 | |||
8b8b377c29 | |||
d70d2cdff5 | |||
b0704063f2 | |||
dde7254dcf | |||
4edb9ed398 | |||
9d764781c3 | |||
f7d991ab54 | |||
59b3b7d79d | |||
354bf362c0 | |||
69304466a7 | |||
6e4d4739a6 | |||
e8947b3107 | |||
153641a249 | |||
60706e8b8b | |||
19dc859ef2 | |||
06ff4444c5 | |||
156cfb0c74 | |||
e49a25b604 | |||
b34a137c07 | |||
1fb6b3775d | |||
b1c9a204c1 | |||
8435be1b1f | |||
7affb9ab63 | |||
5627ad0801 | |||
08017a0cd1 | |||
b6603c3425 | |||
f63323feab | |||
eea5f0ba6a | |||
9f51dfad33 | |||
c31789c112 | |||
86043fbb31 | |||
1136ec2ad4 | |||
7910f6e420 | |||
a087b0bf4b | |||
d0929c1dc5 | |||
46500dcb32 | |||
f85c34b807 | |||
dfc3a5df42 | |||
2b78925c68 | |||
eb2fedd669 | |||
b8b5e70151 | |||
8c00905c99 | |||
598af5ebd5 | |||
8adedee973 | |||
4dc2143160 | |||
aded163e0a | |||
11a553cbf4 | |||
a0048e9397 | |||
a8a22b5803 | |||
4122af491c | |||
0de42b6654 | |||
d7aa1144cf | |||
855f768e29 | |||
87fbda0b29 | |||
99dd9a0750 | |||
c8befdd5ea | |||
54cf97170d | |||
a5aa89512a | |||
b02813f30a | |||
51c8430e9c | |||
a846f7da47 | |||
b4e5443e56 | |||
3fdfd91fff | |||
e4b52c036a | |||
43098aa61b | |||
e4d8fea138 | |||
6da22ea8b0 | |||
5e618e0134 | |||
fd09f64fe6 | |||
ef2241e0cb | |||
bcfe5af135 | |||
a598afec43 | |||
3c9ec48da3 | |||
7ce15caded | |||
7e65e0de0e | |||
07a70d8c92 | |||
038574b107 | |||
24067e1e97 | |||
68b13c6425 | |||
a9de6a123e | |||
b0251b8177 | |||
c069d75ede | |||
5886cb7406 | |||
1713dd4ea0 | |||
f5f7971cb5 | |||
5271abbd1f | |||
7d2ded5944 | |||
d2b0319d63 | |||
c15f4c7fd0 | |||
3aee05bf26 | |||
9d148a70c8 | |||
4a64bb250d | |||
e6f5ea172a | |||
6a0ab3526a | |||
fa29b914cc | |||
3a79f8fb50 | |||
321eb38f2d | |||
46dccf8a2d | |||
7f02a533fa | |||
38d329a601 | |||
1c0f08fc5b | |||
a076deeb5f | |||
a006bc7d93 | |||
2b215524b6 | |||
f497e605b4 | |||
b09f1fff51 | |||
92af538b0a | |||
f0b821b122 | |||
4c5e99142f | |||
b0f39a7d0a | |||
38b469755f | |||
3bfda338ef | |||
6e1da22353 | |||
60a0303d7f | |||
6135b4daae | |||
2fe38192b4 | |||
ef3f4c2e17 | |||
e4e069ccca | |||
505d2e4274 | |||
4cdf2f468c | |||
8c7a2c4262 | |||
156ce14dd8 | |||
27dc6443ec | |||
0a5773211d | |||
b3b0c39163 | |||
bc6b4d4be6 | |||
a14e5a99d9 | |||
d643b947a9 | |||
2033e82ff0 | |||
c2c872d000 | |||
ae9377f8be | |||
93a84cad34 | |||
f9ceb92495 | |||
bfdd5c9ec3 | |||
d800528550 | |||
6ec656b094 | |||
29c5da1494 | |||
0a21a9e5f0 | |||
d3aaa3656b | |||
69a8223d06 | |||
6959370087 | |||
1d0937397f | |||
aeeafe8556 | |||
38c5163ab9 | |||
4646e4b0d9 | |||
e9f105c52a | |||
3b225405cb | |||
b96a870c4e | |||
b70d70e82d | |||
f4d1c27cde | |||
4195b00e48 | |||
0a37dabf4b | |||
2d5ac20c46 | |||
700fd61f34 | |||
a0462fb480 | |||
d9d969406e | |||
f19685787f | |||
60caba86a5 | |||
80fa7ebe6c | |||
9096edfc92 | |||
f30ad78a4c | |||
ce0d99b78f | |||
3ffb80c6f1 | |||
4e3c407583 | |||
aaf9fc5d92 | |||
5043f840a2 | |||
fac3bd1edd | |||
976cd41a87 | |||
4aeaee65c3 | |||
f880a0f1a3 | |||
3a886e39fb | |||
005d43a3ec | |||
1d760f4728 | |||
02e3148efe | |||
e9ada7e5fb | |||
2ce01a1c86 | |||
1af8bc95a8 | |||
0046ed879f | |||
4f8c6455bc | |||
c80b8c8ce9 | |||
5435c2fb53 | |||
88963758b1 | |||
e4d40c7693 | |||
2f928ac034 | |||
509d7b20b4 | |||
35c4d0bf4e | |||
5e5f2b0f1a | |||
58135c965c | |||
5a9b84fe00 | |||
c9a10d9033 | |||
730c316e89 | |||
29a07429df | |||
e44616fb3c | |||
cb6ca49607 | |||
ff8b58ee62 | |||
b1350928ee | |||
52641494d3 | |||
9bfe4ed829 | |||
d6f0c0837b | |||
074a1bbca4 | |||
178cd2b52d | |||
e35f807218 | |||
63ca5cc66b | |||
6b66d4a34c | |||
49580a7630 | |||
0300c1057b | |||
f5b8d3120d | |||
fd88db37f7 | |||
39e36f957d | |||
e84c39bf7b | |||
5641b33bdd | |||
72681bef83 | |||
6de16125f7 | |||
46478546a8 | |||
d4659ffaae | |||
e192806950 | |||
1170334fde | |||
d368064549 | |||
ada0703d17 | |||
06d3a753ac | |||
567c6ea8e7 | |||
bfc0868721 | |||
f5b7a8db1a | |||
bef0a44447 | |||
e9ff202851 | |||
4a3323f7d0 | |||
0014a0ca6c | |||
7e99a9f789 | |||
03e5c5ca10 | |||
e4ff988f4d | |||
dde2320afc | |||
988d508025 | |||
24850450cb | |||
e97d1ac257 | |||
bc44eee5b7 | |||
7a010e3446 | |||
cb4fbb19ed | |||
ddf0357c5c | |||
800c01ea42 | |||
68bfba486e | |||
c09f6cb20c | |||
959bad0247 | |||
efb8555d76 | |||
a393772083 | |||
b4ade8c15d | |||
03502f6533 | |||
42c7669bca | |||
1916751f4f | |||
e46dd808e7 | |||
5e92664761 | |||
aa5fd22db0 | |||
56b1c07f58 | |||
2ecac08bcc | |||
95002a6e87 | |||
cea2b42b41 | |||
8bc5a7d746 | |||
85723b975b | |||
a2e05cf80b | |||
87363e47b0 | |||
aefa7a1a4a | |||
18f90587f2 | |||
f47e9b6e14 | |||
a7e4c1295d | |||
50097eef9f | |||
949cdccbae | |||
3d0dc64de1 | |||
2e0102d689 | |||
2557111607 | |||
46a626d2c5 | |||
fb92b1a0b4 | |||
8e6ed194a6 | |||
01175e687c | |||
00d2d4f969 | |||
43715f9835 | |||
b99229b033 | |||
62df1263b1 | |||
a6f0e7f9b9 | |||
f1472d4f8b | |||
52ad282492 | |||
e7b39f29f2 | |||
c62d3abbc5 | |||
fb3ce226b5 | |||
de5f43713f | |||
84aba68b96 | |||
1be0171398 | |||
82c49a42c3 | |||
599a4c7b17 | |||
895dbbdacb | |||
77026d4eeb | |||
256b1881f0 | |||
2a299485cc | |||
a6909a76ed | |||
1ecba5bb7e | |||
c381b737d3 | |||
70e7974396 | |||
2104b60b81 | |||
09dcb8029c | |||
f2ed26479f | |||
d03d9b344a | |||
9b5fbda9a2 | |||
647fbd1f4b | |||
fa6ed901df | |||
23ac3d18e5 | |||
f18891a07e | |||
c85825f3c7 | |||
319571edf3 | |||
1f20d96191 | |||
c2e50305eb | |||
d00e1284d6 | |||
ef4430216d | |||
643f666178 | |||
989b968e5b | |||
92484e26e5 | |||
429056f2ca | |||
fa2b44eeb3 | |||
52e742049b | |||
97a86269e6 | |||
654d98b5c4 | |||
0509db1935 | |||
acf879d28c | |||
cefe45e8cc | |||
03b44fff22 | |||
71c373926f | |||
1af11f076f | |||
41c99c33a6 | |||
58cce53746 | |||
df6cafd6d1 | |||
cc6c1b5641 | |||
d61adc0259 | |||
9ed7ee65c3 | |||
5f94e230f9 | |||
4d8f44ddae | |||
b20c028566 | |||
9b883b1a09 | |||
53402459f2 | |||
bc6a1a1da6 | |||
690d2fb15b | |||
7ac188709b | |||
93c8773e91 | |||
eefe95c2ee | |||
af9825d369 | |||
59f38cb343 | |||
810d2d4776 | |||
027cb10d0f | |||
24a6eba94e | |||
c1c43bb6fb | |||
0804550539 | |||
3156482334 | |||
052317b38a | |||
6ff684f638 | |||
6857c8c1fe | |||
44d563e689 | |||
6ce2809450 | |||
0759036536 | |||
6f6490b7ac | |||
da12a4b17f | |||
1302bc84dd | |||
1c4a8046d0 | |||
141edac99b | |||
b64ecfb836 | |||
d5f4db5eb7 | |||
7a3f6e9f63 | |||
b3ecc91be9 | |||
2a27eacf5e | |||
e747686ad8 | |||
282af20146 | |||
f25cd9fdbf | |||
2369547d59 | |||
d86b8cc78c | |||
b4c1e6a44c | |||
45fc2f2e0e | |||
9cfcacf45e | |||
801fd83649 | |||
16f1dcd922 | |||
8f36d698cf | |||
a89a36fd4c | |||
8a7f8c4068 | |||
9c70e69300 | |||
a0b490b10f | |||
d69dc375a3 | |||
ac6dbbcd89 | |||
14879c4466 | |||
dce08d4922 | |||
5195cb8eda | |||
68de7b516e | |||
10095205d8 | |||
c4c988f7a4 | |||
333dfbc642 | |||
e915fd28cb | |||
46636b537c | |||
cd5545284e | |||
3dac6366ff | |||
054a26cf65 | |||
024c76d480 | |||
9abdfeac77 | |||
0f12ae0cf7 | |||
71182e664a | |||
7fc6d53cae | |||
4f2985469c | |||
9cc24a3be3 | |||
7ec18861bf | |||
3d1c02136a | |||
aca34155b9 | |||
d04ffb7ea9 | |||
5b3e72db54 | |||
f811cc5c87 | |||
e7abe27bb6 | |||
126d875547 | |||
a2b7228e95 | |||
10d6b3a6ca | |||
4c9be5557d | |||
37e0ac0895 | |||
8934d251d9 | |||
b5263a6968 | |||
15bd8e964d | |||
d931027067 | |||
39ffd3c3bc | |||
9ff8235de4 | |||
54075733b7 | |||
2ea0538825 | |||
275e20eabd | |||
7fe742e6ed | |||
9a6bb69df8 | |||
4c93ca6914 | |||
51939ea652 | |||
256a4a2e7f | |||
c217cab5db | |||
c93b6deaf8 | |||
55196b86b3 | |||
c5ab79f618 | |||
59fcfbe9d2 | |||
6ce70296a6 | |||
fdef687f0c | |||
cdd7f42532 | |||
65955fd7dc | |||
b785f68154 | |||
895191814e | |||
b06f253f83 | |||
0814439886 | |||
a978cb90e8 | |||
39e0d434ad | |||
6ada3cbf59 | |||
4aff768b8e | |||
e08e569135 | |||
8cac0fca67 | |||
f447b06eff | |||
78b2045b14 | |||
a427401d66 | |||
6da6ca710e | |||
f46c42f522 | |||
c4df7b496b | |||
6c4332f5c2 | |||
86076d890a | |||
76a041f504 | |||
c9bdc72d5d | |||
f6a8b23498 | |||
2a60c828d7 | |||
ea7ff432b2 | |||
5c2fbec80a | |||
f0109aa796 | |||
66b92f3eb7 | |||
ec4af65c36 | |||
852c1a423d | |||
1904be277d | |||
ebb1022100 | |||
8d85ec3cd9 | |||
1d36c3269e | |||
a218f4a48b | |||
b14c3cc49f | |||
cbc4a0f3ab | |||
70a024d3de | |||
28d43bbecc | |||
55a3b2f3a1 | |||
2545b22ab1 | |||
77c07d13c0 | |||
98ac8a69c2 | |||
0e22af7ba0 | |||
6951ce34c7 | |||
24e10d6037 | |||
f1b08bf56e | |||
3cc3799ebb | |||
aed20a790e | |||
81b20a23bf | |||
bd27cb74a7 | |||
07fd3575c5 | |||
6da504c999 | |||
23c1827838 | |||
49833fcc48 | |||
0e4c0fba5c | |||
bfe67f1cdf | |||
863e349711 | |||
5e13df9bd8 | |||
65abb8fa6c | |||
fe5c0295c3 | |||
4d15ac2fa3 | |||
5e4b9fb15b | |||
8a70dffae8 | |||
8119eb4b34 | |||
1bae7ba8f5 | |||
d4f1014df6 | |||
dce685e711 | |||
c4e6668c22 | |||
9b5608c86d | |||
2485a00d34 | |||
8abe2d76b0 | |||
8e9087226f | |||
8ccc3c8b0c | |||
9b3579acf6 | |||
6f2ff6a77e | |||
f772501159 | |||
cadd389658 | |||
6f36331818 | |||
d684eb5147 | |||
349546cf87 | |||
352593ebc6 | |||
c6b28cbcaf | |||
c5e69d2080 | |||
cd4964d086 | |||
ac3967e997 | |||
5df18f2558 | |||
0b054126bc | |||
d4740c57be | |||
739fc9f95d | |||
96b1340aec | |||
57d83e3d1b | |||
4e2c9dc083 | |||
fa6f60d454 | |||
71fe2bda97 | |||
f24c5e944e | |||
603fd84753 | |||
5fbe1a8614 | |||
42169783e1 | |||
90574937df | |||
ec0fe2210d | |||
450fcccaa6 | |||
72504ca53f | |||
6bfddb3ece | |||
b62fcbc2ec | |||
411dda0e75 | |||
d9d71c8745 | |||
35239af039 | |||
840e571917 | |||
2fd4204db8 | |||
1fd105ac56 | |||
6ae90e0a7d | |||
43dc12a0f3 | |||
28d05b629f | |||
30c12fc9de | |||
cdb62f502c | |||
5cfdbbce7a | |||
e2af1af656 | |||
a96c25c37c | |||
c1abec3b4c | |||
c5a488ca3f | |||
dd4a18a0b2 | |||
34df0759f3 | |||
b6a3f6ebd2 | |||
ad2819a3bc | |||
0f2be86d5a | |||
4342584c32 | |||
71c10df270 | |||
db90b5d2cb | |||
5cc38c1369 | |||
e7c48a98df | |||
d03c49db58 | |||
b69af710ad | |||
4d3b469c48 | |||
8cc6c0171b | |||
8be5be4720 | |||
995a1155e4 | |||
234c3bb72a | |||
aafe863774 | |||
fb1db914aa | |||
7aa8abdd98 | |||
4bd965a795 | |||
df2a4779bf | |||
d2dc063c8e | |||
45f4c63941 | |||
5e968e5651 | |||
3e3c0a1f14 | |||
c48bebe0b2 | |||
0dd9e9e015 | |||
d2a2e17e91 | |||
09dbd723e4 | |||
eb965542cc | |||
615fa05a75 | |||
87a2ac7887 | |||
07ce90ab8c | |||
a71ae29c98 | |||
117214c671 | |||
c54d26d6ba | |||
5447bd098b | |||
2b7c0e8e80 | |||
47966d89f2 | |||
b28a2c3bd4 | |||
8f51abfc97 | |||
e9bea8ed47 | |||
c2c1b6d2e8 | |||
ded22f1717 | |||
126da3979c | |||
d892f2f7f4 | |||
f18b32626a | |||
8c8f2585aa | |||
1a811d0917 | |||
9af552c15a | |||
263cc1d97c | |||
dec4471871 | |||
c8122ebd68 | |||
b766ddea54 | |||
5cb219d83e | |||
908128b55d | |||
b240960a8a | |||
b91d8e4c19 | |||
4853ff7eee | |||
e1582bd73a | |||
ac17335dbe | |||
7053777997 | |||
1107b95ebd | |||
36003fd622 | |||
84121ff901 | |||
bcc2ec1668 | |||
08dffda2a1 | |||
9f4540a4f1 | |||
f19ef9aa01 | |||
f10a06341b | |||
67ac95f15f | |||
cd291f0a27 | |||
bb6b88a703 | |||
239b36c31a | |||
dcb6ae44dd | |||
5ed365cf0d | |||
b20b3d6f5c | |||
32d02f9329 | |||
3da7c47bf5 | |||
029f159aea | |||
0a300822a4 | |||
8d48e0289a | |||
9dbb59f337 | |||
a243aeafba | |||
5c671de29e | |||
e161e5d30d | |||
2fd64d0ca7 | |||
d981c75600 | |||
492adb7035 | |||
811bca18d9 | |||
3222e0646d | |||
fa6790856d | |||
38a15d4158 | |||
77cd459c51 | |||
fa115ed9a0 | |||
30a2b572ab | |||
b1ab77188d | |||
e212e4581d | |||
1b8daa7e09 | |||
99d126fd3a | |||
9fee7c6b19 | |||
1b5a83563f | |||
c8a8eb0a4d | |||
0a7812bb2c | |||
061b434687 | |||
f3be1a6d09 | |||
1e797bfe8a | |||
97435dbe0f | |||
3fdbb95b15 | |||
961a6c0b4f | |||
c42f011a05 | |||
957c50088d | |||
3b129176d6 | |||
f36327ecc9 | |||
5f48bb8e7d | |||
aba8d01818 | |||
03cb7062f2 | |||
d4b35736c0 | |||
c30aa710f3 | |||
0adac4217e | |||
7e102edd0a | |||
adefe87129 | |||
8a05fc8966 | |||
8fa798cf63 | |||
01b20b2acb | |||
a8d72dd6f8 | |||
84042c61e0 | |||
407ba6c628 | |||
16a3c676ef | |||
9b3a99432b | |||
036f757086 | |||
a391529b32 | |||
d10b782308 | |||
a58f780452 | |||
606de03c7c | |||
2b988c51ad | |||
cb73e55db1 | |||
8fb4093dc6 | |||
f5c4535cb0 | |||
e4f2bffbc2 | |||
5c942460f9 | |||
cce1f2c20d | |||
516a3bd017 | |||
63d58c7a4f | |||
c49b865ee7 | |||
097a83e3ba | |||
02c70d868d | |||
c836f52460 | |||
caa1e1ef09 | |||
5c63531066 | |||
15b2bbc6d1 | |||
3cafbd9141 | |||
dc0405a373 | |||
f84832716a | |||
200aa4042f | |||
f44c5adcc0 | |||
3c43bebe64 |
33
.github/CONTRIBUTING.md
vendored
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
|
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||||
|
3. What is your type of issue?
|
||||||
|
* [Catalogue request](#catalogue-requests)
|
||||||
|
* [Bugs](#bugs)
|
||||||
|
* [Feature requests](#feature-requests)
|
||||||
|
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
||||||
|
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
# Catalogue requests
|
||||||
|
|
||||||
|
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||||
|
|
||||||
|
# Bugs
|
||||||
|
* Include version (Setting > About > Version)
|
||||||
|
* If not latest, try updating, it may have already been solved
|
||||||
|
* Dev version is equal to the number of commits as seen in the main page
|
||||||
|
* Include steps to reproduce (if not obvious from description)
|
||||||
|
* Include screenshot (if needed)
|
||||||
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
|
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||||
|
|
||||||
|
# Feature requests
|
||||||
|
|
||||||
|
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||||
|
* Include screenshot (if needed)
|
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
|||||||
|
github: inorichi
|
||||||
ko_fi: inorichi
|
ko_fi: inorichi
|
||||||
|
16
.github/ISSUE_TEMPLATE.md
vendored
Normal file → Executable file
@ -2,21 +2,15 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
- To the latest version of the app (stable is v0.12.1)
|
- I have updated all extensions
|
||||||
- All extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Device information
|
### Device information
|
||||||
* Tachiyomi version: ?
|
* Tachiyomi version: ?
|
||||||
* Android version: ?
|
* Android version: ?
|
||||||
* Device: ?
|
* Device: ?
|
||||||
@ -30,5 +24,3 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
|||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
|
||||||
|
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
about: Report a bug
|
||||||
|
title: "[Bug] Write short description here"
|
||||||
|
labels: "bug"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
|
- I have updated all extensions
|
||||||
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
This should happen.
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
This happened instead.
|
||||||
|
|
||||||
|
### Other details
|
||||||
|
Additional details and attachments.
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: ⚠️ Extension/source issue
|
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
|
||||||
- name: 📦 Tachiyomi extensions
|
|
||||||
url: https://tachiyomi.org/extensions
|
|
||||||
about: List of all available extensions with download links
|
|
||||||
- name: 🖥️ Tachiyomi website
|
|
||||||
url: https://tachiyomi.org/help/
|
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
|
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "🌟 Feature request"
|
||||||
|
about: Suggest a feature to improve Tachiyomi
|
||||||
|
title: "[Feature Request] Write short description here"
|
||||||
|
labels: "feature"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v0.9.2)
|
||||||
|
- I have updated all extensions
|
||||||
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why/User Benefit/User Problem
|
||||||
|
(explain why this feature should be added)
|
||||||
|
|
||||||
|
### What/Requirements
|
||||||
|
(explain how this feature would behave)
|
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -1,106 +0,0 @@
|
|||||||
name: 🐞 Issue report
|
|
||||||
description: Report an issue in Tachiyomi
|
|
||||||
labels: [Bug]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I have updated all installed extensions.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: tachiyomi-version
|
|
||||||
attributes:
|
|
||||||
label: Tachiyomi version
|
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
|
||||||
placeholder: |
|
|
||||||
Example: "0.11.1"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: android-version
|
|
||||||
attributes:
|
|
||||||
label: Android version
|
|
||||||
description: You can find this somewhere in your Android settings.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Android 11"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: device
|
|
||||||
attributes:
|
|
||||||
label: Device
|
|
||||||
description: List your device and model.
|
|
||||||
placeholder: |
|
|
||||||
Example: "Google Pixel 5"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce-steps
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce
|
|
||||||
description: Provide an example of the issue.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
3. Issue here
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: Expected behavior
|
|
||||||
description: Explain what you should expect to happen.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This should happen..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual-behavior
|
|
||||||
attributes:
|
|
||||||
label: Actual behavior
|
|
||||||
description: Explain what actually happens.
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"This happened instead..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: crash-logs
|
|
||||||
attributes:
|
|
||||||
label: Crash logs
|
|
||||||
description: |
|
|
||||||
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
|
||||||
placeholder: |
|
|
||||||
You can paste the crash logs in pure text or upload it as an attachment.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -1,39 +0,0 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Suggest a feature to improve Tachiyomi
|
|
||||||
labels: [Feature request]
|
|
||||||
body:
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
|
||||||
options:
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
|
||||||
required: true
|
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
|
||||||
required: true
|
|
||||||
- label: I have updated the app to version **[0.12.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: feature-description
|
|
||||||
attributes:
|
|
||||||
label: Describe your suggested feature
|
|
||||||
description: How can Tachiyomi be improved?
|
|
||||||
placeholder: |
|
|
||||||
Example:
|
|
||||||
"It should work like this..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: other-details
|
|
||||||
attributes:
|
|
||||||
label: Other details
|
|
||||||
placeholder: |
|
|
||||||
Additional details and attachments.
|
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: "Extension/source/catalogue issue"
|
||||||
|
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||||
|
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||||
|
labels: "catalog"
|
||||||
|
---
|
||||||
|
|
||||||
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
BIN
.github/readme-images/app-icon.png
vendored
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
.github/readme-images/screens.png
vendored
Before Width: | Height: | Size: 454 KiB After Width: | Height: | Size: 1.0 MiB |
6
.github/runner-files/ci-gradle.properties
vendored
@ -1,6 +0,0 @@
|
|||||||
org.gradle.daemon=false
|
|
||||||
org.gradle.jvmargs=-Xmx5120m
|
|
||||||
org.gradle.workers.max=2
|
|
||||||
|
|
||||||
kotlin.incremental=false
|
|
||||||
kotlin.compiler.execution.strategy=in-process
|
|
25
.github/workflows/PullRequestTester.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Pull Request Checker
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
apk:
|
||||||
|
name: Generate APK
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
- name: Get NDK
|
||||||
|
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570"
|
||||||
|
- name: Build Release APK
|
||||||
|
run: bash ./gradlew assembleDebug --stacktrace
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: TachiyomiSY-${{ github.sha }}.apk
|
||||||
|
path: app/build/outputs/apk/dev/debug/app-dev-debug.apk
|
31
.github/workflows/TachiyomiSY-Preview-Builder.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
name: Remote Dispatch Action Initiator
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ping-pong:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
with:
|
||||||
|
fetch-depth: '0'
|
||||||
|
- name: TAG - Bump version and push tag
|
||||||
|
uses: anothrNick/github-tag-action@1.17.2
|
||||||
|
if: github.event.action != 'pong'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
WITH_V: true
|
||||||
|
RELEASE_BRANCHES: master
|
||||||
|
- name: PING - Dispatch initiating repository event
|
||||||
|
if: github.event.action != 'pong'
|
||||||
|
run: |
|
||||||
|
curl -X POST https://api.github.com/repos/jobobby04/TachiyomiSYPreview/dispatches \
|
||||||
|
-H 'Accept: application/vnd.github.everest-preview+json' \
|
||||||
|
-u ${{ secrets.ACCESS_TOKEN }} \
|
||||||
|
--data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}'
|
||||||
|
- name: ACK - Acknowledge pong from remote repository
|
||||||
|
if: github.event.action == 'pong'
|
||||||
|
run: |
|
||||||
|
echo "PONG received from '${{ github.event.client_payload.repository }}'"
|
95
.github/workflows/build.yml
vendored
@ -1,95 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_wrapper:
|
|
||||||
name: Validate Gradle Wrapper
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@v1
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
needs: check_wrapper
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Cancel previous runs
|
|
||||||
uses: styfle/cancel-workflow-action@0.5.0
|
|
||||||
with:
|
|
||||||
access_token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up JDK 11
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 11
|
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
uses: eskatos/gradle-command-action@v1
|
|
||||||
with:
|
|
||||||
arguments: assembleStandardRelease
|
|
||||||
wrapper-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
id: get_tag_name
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# TODO: need to support multiple APKs
|
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/standard/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Clean up build artifacts
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
run: |
|
|
||||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
|
||||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
|
||||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.VERSION_TAG }}
|
|
||||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
|
||||||
body: |
|
|
||||||
MD5: ${{ env.APK_MD5 }}
|
|
||||||
files: |
|
|
||||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
31
.github/workflows/issue_closer.yml
vendored
@ -1,32 +1,13 @@
|
|||||||
name: Issue closer
|
name: Issue closer
|
||||||
on:
|
on: [issues]
|
||||||
issues:
|
|
||||||
types: [opened, edited, reopened]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose issues
|
- name: Autoclose issue
|
||||||
uses: arkon/issue-closer-action@v3.4
|
uses: arkon/issue-closer-action@v1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
issue-close-message: "@${issue.user.login} this issue was automatically closed because it was not filled in correctly or the acknowledgment section was not removed."
|
||||||
[
|
issue-title-pattern: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||||
{
|
issue-body-pattern: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||||
"type": "body",
|
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
|
||||||
"message": "The acknowledgment section was not removed."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "body",
|
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
|
||||||
"message": "Requested information in the template was not filled out."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "both",
|
|
||||||
"regex": ".*(aniyomi|anime).*",
|
|
||||||
"ignoreCase": true,
|
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
14
.github/workflows/issue_moderator.yml
vendored
@ -1,14 +0,0 @@
|
|||||||
name: Issue moderator
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
moderate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Moderate issues
|
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.1
|
|
||||||
with:
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
19
.github/workflows/lock.yml
vendored
@ -1,19 +0,0 @@
|
|||||||
name: Lock threads
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Daily
|
|
||||||
schedule:
|
|
||||||
- cron: '0 * * * *'
|
|
||||||
# Manual trigger
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lock:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/lock-threads@v2
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
issue-lock-inactive-days: '2'
|
|
||||||
pr-lock-inactive-days: '2'
|
|
7
.gitignore
vendored
Normal file → Executable file
@ -5,6 +5,8 @@
|
|||||||
.idea/
|
.idea/
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
/mainframer
|
||||||
|
/.mainframer
|
||||||
|
|
||||||
# Built files
|
# Built files
|
||||||
*/build
|
*/build
|
||||||
@ -14,3 +16,8 @@ app/**/output.json
|
|||||||
|
|
||||||
# Hebrew assets are copied on build
|
# Hebrew assets are copied on build
|
||||||
app/src/main/res/values-iw/
|
app/src/main/res/values-iw/
|
||||||
|
|
||||||
|
TODO.md
|
||||||
|
CHANGELOG.md
|
||||||
|
/captures
|
||||||
|
build.sh
|
||||||
|
81
.travis.yml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
dist: trusty
|
||||||
|
language: android
|
||||||
|
|
||||||
|
android:
|
||||||
|
components:
|
||||||
|
- tools
|
||||||
|
- platform-tools
|
||||||
|
- build-tools-29.0.3
|
||||||
|
- android-29
|
||||||
|
- extra-android-m2repository
|
||||||
|
- extra-google-m2repository
|
||||||
|
- extra-android-support
|
||||||
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
- 'android-sdk-license-.+'
|
||||||
|
- 'android-sdk-preview-license-.+'
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
|
||||||
|
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||||
|
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||||
|
tar xf secrets.tar;
|
||||||
|
mv debug.keystore "$HOME/.android";
|
||||||
|
fi
|
||||||
|
- mkdir "$ANDROID_HOME/licenses" || true
|
||||||
|
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||||
|
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||||
|
|
||||||
|
install:
|
||||||
|
- echo y | sdkmanager "ndk-bundle"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
||||||
|
|
||||||
|
script: ".travis/build.sh"
|
||||||
|
|
||||||
|
before_cache:
|
||||||
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
|
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- "$HOME/.gradle/caches/"
|
||||||
|
- "$HOME/.gradle/wrapper/"
|
||||||
|
- "$HOME/.android/build-cache"
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
- provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||||
|
file: tachiyomi-v*.apk
|
||||||
|
file_glob: true
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
repo: inorichi/tachiyomi
|
||||||
|
- provider: script
|
||||||
|
script: ".travis/deploy.sh"
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
condition: "-z $TRAVIS_TAG"
|
||||||
|
repo: inorichi/tachiyomi
|
||||||
|
- provider: script
|
||||||
|
script: ".travis/deploy.sh"
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
branch: dev
|
||||||
|
condition: "-z $TRAVIS_TAG"
|
||||||
|
repo: inorichi/tachiyomi
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
||||||
|
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
||||||
|
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
||||||
|
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
||||||
|
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
||||||
|
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
||||||
|
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
@ -1,76 +0,0 @@
|
|||||||
# Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
|
||||||
https://www.contributor-covenant.org/faq
|
|
@ -1,34 +0,0 @@
|
|||||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Thanks for your interest in contributing to Tachiyomi!
|
|
||||||
|
|
||||||
|
|
||||||
# Code contributions
|
|
||||||
|
|
||||||
Pull requests are welcome!
|
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
|
||||||
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
|
|
||||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
|
||||||
|
|
||||||
|
|
||||||
# Forks
|
|
||||||
|
|
||||||
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
|
|
||||||
|
|
||||||
When creating a fork, remember to:
|
|
||||||
|
|
||||||
- To avoid confusion with the main app:
|
|
||||||
- Change the app name
|
|
||||||
- Change the app icon
|
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
|
||||||
- To avoid installation conflicts:
|
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
|
||||||
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
|
||||||
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
|
26
LICENSE
Normal file → Executable file
@ -174,3 +174,29 @@
|
|||||||
of your accepting any such warranty or additional liability.
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright {yyyy} {name of copyright owner}
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
97
README.md
@ -1,96 +1,3 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
I haven't started a readme
|
||||||
|-------|----------|---------|------------|---------|
|
|
||||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
|
||||||
|
|
||||||
|
Automated Preview Builds(with updater): https://github.com/jobobby04/TachiyomiSYPreview/releases
|
||||||
# Tachiyomi
|
|
||||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
Features include:
|
|
||||||
* Online reading from a variety of sources
|
|
||||||
* Local reading of downloaded content
|
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
|
||||||
* Categories to organize your library
|
|
||||||
* Light and dark themes
|
|
||||||
* Schedule updating your library for new chapters
|
|
||||||
* Create backups locally to read offline or to your desired cloud service
|
|
||||||
|
|
||||||
## Download
|
|
||||||
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/tachiyomi-preview/releases).
|
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
|
||||||
|
|
||||||
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
|
|
||||||
|
|
||||||
<details><summary>Issues</summary>
|
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Bugs</summary>
|
|
||||||
|
|
||||||
* Include version (More > About > Version)
|
|
||||||
* If not latest, try updating, it may have already been solved
|
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
|
||||||
* Include steps to reproduce (if not obvious from description)
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
|
||||||
* Don't group unrelated requests into one issue
|
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
|
||||||
|
|
||||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Feature Requests</summary>
|
|
||||||
|
|
||||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
|
||||||
* Include screenshot (if needed)
|
|
||||||
|
|
||||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Contributing</summary>
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details><summary>Code of Conduct</summary>
|
|
||||||
|
|
||||||
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
[See our website.](https://tachiyomi.org/)
|
|
||||||
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright 2015 Javier Tomás
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
The developer of this application does not have any affiliation with the content providers available.
|
|
||||||
|
2
app/.gitignore
vendored
Normal file → Executable file
@ -2,3 +2,5 @@
|
|||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
custom.gradle
|
custom.gradle
|
||||||
|
google-services.json
|
||||||
|
output.json
|
374
app/build.gradle
Executable file
@ -0,0 +1,374 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||||
|
//noinspection GradleDependency
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||||
|
// Realm (EH)
|
||||||
|
apply plugin: 'realm-android'
|
||||||
|
|
||||||
|
shortcutHelper.filePath = './shortcuts.xml'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
// Git is needed in your system PATH for these commands to work.
|
||||||
|
// If it's not installed, you can return a random value as a workaround
|
||||||
|
getCommitCount = {
|
||||||
|
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
getGitSha = {
|
||||||
|
return 'git rev-parse --short HEAD'.execute().text.trim()
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuildTime = {
|
||||||
|
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||||
|
return df.format(new Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 29
|
||||||
|
buildToolsVersion '29.0.3'
|
||||||
|
publishNonDefault true
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "eu.kanade.tachiyomi.sy"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 29
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.9.2.7"
|
||||||
|
|
||||||
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
|
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
ext.enableCrashlytics = false
|
||||||
|
}
|
||||||
|
releaseTest {
|
||||||
|
applicationIdSuffix ".rt"
|
||||||
|
// minifyEnabled true
|
||||||
|
// shrinkResources true
|
||||||
|
zipAlignEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
zipAlignEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "default"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
standard {
|
||||||
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
|
dimension "default"
|
||||||
|
}
|
||||||
|
dev {
|
||||||
|
resConfigs "en", "xxhdpi"
|
||||||
|
dimension "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility 1.8
|
||||||
|
targetCompatibility 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/DEPENDENCIES'
|
||||||
|
exclude 'LICENSE.txt'
|
||||||
|
exclude 'META-INF/LICENSE'
|
||||||
|
exclude 'META-INF/LICENSE.txt'
|
||||||
|
exclude 'META-INF/NOTICE'
|
||||||
|
exclude 'META-INF/*.kotlin_module'
|
||||||
|
|
||||||
|
// Compatibility for two RxJava versions (EXH)
|
||||||
|
exclude 'META-INF/rxjava.properties'
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
checkReleaseBuilds false
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
targetCompatibility = 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
// Modified dependencies
|
||||||
|
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||||
|
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
|
// Android support library
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
|
implementation 'androidx.browser:browser:1.2.0'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
|
|
||||||
|
final lifecycle_version = '2.2.0'
|
||||||
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
|
// UI library
|
||||||
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
|
|
||||||
|
standardImplementation 'com.google.firebase:firebase-core:17.4.1'
|
||||||
|
|
||||||
|
// ReactiveX
|
||||||
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
|
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
|
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||||
|
|
||||||
|
// Network client
|
||||||
|
final okhttp_version = '4.7.2'
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
|
implementation 'com.squareup.okio:okio:2.6.0'
|
||||||
|
|
||||||
|
// REST
|
||||||
|
final retrofit_version = '2.9.0'
|
||||||
|
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
|
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
|
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
|
// JavaScript engine
|
||||||
|
implementation 'com.squareup.duktape:duktape-android:1.2.0' // Stuck on 1.2.0 to fix MangaPlus extension
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation 'org.jsoup:jsoup:1.13.1'
|
||||||
|
|
||||||
|
// Job scheduling
|
||||||
|
final work_version = '2.3.4'
|
||||||
|
implementation "androidx.work:work-runtime:$work_version"
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
|
||||||
|
// [EXH] Android 7 SSL Workaround
|
||||||
|
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
|
||||||
|
|
||||||
|
// Changelog
|
||||||
|
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
|
// Database
|
||||||
|
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||||
|
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||||
|
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||||
|
implementation 'io.requery:sqlite-android:3.31.0'
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
|
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||||
|
|
||||||
|
// Model View Presenter
|
||||||
|
final nucleus_version = '3.0.0'
|
||||||
|
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
|
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
|
// Dependency injection
|
||||||
|
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||||
|
|
||||||
|
// Image library
|
||||||
|
final glide_version = '4.10.0'
|
||||||
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||||
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
final acra_version = '5.5.0'
|
||||||
|
implementation "ch.acra:acra-http:$acra_version"
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
|
|
||||||
|
// UI
|
||||||
|
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
|
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
|
implementation 'eu.davidea:flexible-adapter:5.1.0'
|
||||||
|
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
||||||
|
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
|
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
||||||
|
implementation 'com.github.mthli:Slice:v1.3'
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
|
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
||||||
|
|
||||||
|
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||||
|
final material_dialogs_version = '3.1.1'
|
||||||
|
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
|
||||||
|
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
|
||||||
|
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
|
||||||
|
|
||||||
|
// Conductor
|
||||||
|
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||||
|
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||||
|
exclude group: "com.android.support"
|
||||||
|
}
|
||||||
|
implementation 'com.github.inorichi:conductor-support-preference:a32c357'
|
||||||
|
|
||||||
|
// FlowBinding
|
||||||
|
final flowbinding_version = '0.11.1'
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
||||||
|
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.assertj:assertj-core:3.12.2'
|
||||||
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
|
||||||
|
final robolectric_version = '3.1.4'
|
||||||
|
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||||
|
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||||
|
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
|
||||||
|
final coroutines_version = '1.3.7'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
|
||||||
|
|
||||||
|
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
|
||||||
|
|
||||||
|
// Text distance (EH)
|
||||||
|
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||||
|
|
||||||
|
// Pin lock view (EH)
|
||||||
|
implementation 'com.github.jawnnypoo:pinlockview:2.2.0'
|
||||||
|
|
||||||
|
// Reprint (EH)
|
||||||
|
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
|
||||||
|
implementation 'com.github.ajalt.reprint:rxjava:3.2.1@aar' // optional: the RxJava 1 interface
|
||||||
|
|
||||||
|
// Swirl (EH)
|
||||||
|
implementation 'com.mattprecious.swirl:swirl:1.2.0'
|
||||||
|
|
||||||
|
// RxJava 2 interop for Realm (EH)
|
||||||
|
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
|
||||||
|
|
||||||
|
// Firebase (EH)
|
||||||
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||||
|
|
||||||
|
// Better logging (EH)
|
||||||
|
implementation 'com.elvishew:xlog:1.6.1'
|
||||||
|
|
||||||
|
// Time utils (EH)
|
||||||
|
def typed_time_version = '1.0.2'
|
||||||
|
implementation "com.github.kizitonwose.time:time:$typed_time_version"
|
||||||
|
implementation "com.github.kizitonwose.time:time-android:$typed_time_version"
|
||||||
|
|
||||||
|
// Debug utils (EH)
|
||||||
|
debugImplementation 'com.ms-square:debugoverlay:1.1.3'
|
||||||
|
releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3'
|
||||||
|
releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
||||||
|
testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3'
|
||||||
|
|
||||||
|
// Humanize (EH)
|
||||||
|
implementation 'com.github.mfornos:humanize-slim:1.2.2'
|
||||||
|
|
||||||
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
|
||||||
|
final def markwon_version = '4.1.0'
|
||||||
|
|
||||||
|
implementation "io.noties.markwon:core:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:html:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:image:$markwon_version"
|
||||||
|
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||||
|
|
||||||
|
implementation 'com.google.guava:guava:27.0.1-android'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.3.72'
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
|
tasks.withType(AbstractKotlinCompile).all {
|
||||||
|
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
|
task copyResources(type: Copy) {
|
||||||
|
from './src/main/res/values-he'
|
||||||
|
into './src/main/res/values-iw'
|
||||||
|
include '**/*'
|
||||||
|
}
|
||||||
|
|
||||||
|
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||||
|
|
||||||
|
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
// Firebase (EH)
|
||||||
|
apply plugin: 'io.fabric'
|
||||||
|
}
|
@ -1,338 +0,0 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
|
||||||
kotlin("android")
|
|
||||||
kotlin("plugin.serialization")
|
|
||||||
id("com.github.zellius.shortcut-helper")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|
||||||
apply(plugin = "com.google.gms.google-services")
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
|
||||||
|
|
||||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk = AndroidConfig.compileSdk
|
|
||||||
ndkVersion = AndroidConfig.ndk
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
|
||||||
minSdk = AndroidConfig.minSdk
|
|
||||||
targetSdk = AndroidConfig.targetSdk
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
versionCode = 67
|
|
||||||
versionName = "0.12.1"
|
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
|
||||||
|
|
||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters += SUPPORTED_ABIS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
isEnable = false
|
|
||||||
reset()
|
|
||||||
include(*SUPPORTED_ABIS.toTypedArray())
|
|
||||||
isUniversalApk = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
named("debug") {
|
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
|
||||||
applicationIdSuffix = ".debug"
|
|
||||||
|
|
||||||
isShrinkResources = true
|
|
||||||
isMinifyEnabled = true
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
create("debugFull") { // Debug without R8
|
|
||||||
initWith(getByName("debug"))
|
|
||||||
isShrinkResources = false
|
|
||||||
isMinifyEnabled = false
|
|
||||||
}
|
|
||||||
named("release") {
|
|
||||||
isShrinkResources = true
|
|
||||||
isMinifyEnabled = true
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions("default")
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
create("standard") {
|
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
|
||||||
dimension = "default"
|
|
||||||
}
|
|
||||||
create("dev") {
|
|
||||||
resConfigs("en", "xxhdpi")
|
|
||||||
dimension = "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude("META-INF/DEPENDENCIES")
|
|
||||||
exclude("LICENSE.txt")
|
|
||||||
exclude("META-INF/LICENSE")
|
|
||||||
exclude("META-INF/LICENSE.txt")
|
|
||||||
exclude("META-INF/NOTICE")
|
|
||||||
exclude("META-INF/*.kotlin_module")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
includeInApk = false
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
|
||||||
isAbortOnError = false
|
|
||||||
isCheckReleaseBuilds = false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.5.1"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
|
||||||
|
|
||||||
// AndroidX libraries
|
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
|
||||||
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
|
||||||
|
|
||||||
// Job scheduling
|
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
|
||||||
|
|
||||||
// UI library
|
|
||||||
implementation("com.google.android.material:material:1.5.0-alpha01")
|
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
|
||||||
|
|
||||||
// ReactiveX
|
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
|
||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
|
||||||
|
|
||||||
// Network client
|
|
||||||
val okhttpVersion = "4.9.1"
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
|
||||||
|
|
||||||
// JSON
|
|
||||||
val kotlinSerializationVersion = "1.2.2"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
|
||||||
implementation("com.google.code.gson:gson:2.8.7")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
// JavaScript engine
|
|
||||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
|
||||||
|
|
||||||
// Disk
|
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation("org.jsoup:jsoup:1.14.1")
|
|
||||||
|
|
||||||
// Database
|
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
|
||||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
|
||||||
|
|
||||||
// Preferences
|
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
|
||||||
|
|
||||||
// Model View Presenter
|
|
||||||
val nucleusVersion = "3.0.0"
|
|
||||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
|
||||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
|
||||||
|
|
||||||
// Dependency injection
|
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
|
||||||
|
|
||||||
// Image library
|
|
||||||
val coilVersion = "1.3.2"
|
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
|
||||||
exclude(module = "image-decoder")
|
|
||||||
}
|
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
|
||||||
|
|
||||||
// Crash reports
|
|
||||||
implementation("ch.acra:acra-http:5.8.1")
|
|
||||||
|
|
||||||
// Sort
|
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
|
||||||
|
|
||||||
// UI
|
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
|
||||||
|
|
||||||
// Conductor
|
|
||||||
val conductorVersion = "3.0.0"
|
|
||||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
|
||||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
|
||||||
|
|
||||||
// FlowBinding
|
|
||||||
val flowbindingVersion = "1.2.0"
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
|
||||||
|
|
||||||
// Licenses
|
|
||||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
|
||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
|
||||||
withType<KotlinCompile> {
|
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
|
||||||
"-Xopt-in=kotlin.Experimental",
|
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
|
||||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
|
||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
|
||||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
|
||||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
|
||||||
from("./src/main/res/values-he")
|
|
||||||
into("./src/main/res/values-iw")
|
|
||||||
include("**/*")
|
|
||||||
}
|
|
||||||
|
|
||||||
preBuild {
|
|
||||||
dependsOn(formatKotlin, copyHebrewStrings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Git is needed in your system PATH for these commands to work.
|
|
||||||
// If it's not installed, you can return a random value as a workaround
|
|
||||||
fun getCommitCount(): String {
|
|
||||||
return runCommand("git rev-list --count HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGitSha(): String {
|
|
||||||
return runCommand("git rev-parse --short HEAD")
|
|
||||||
// return "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBuildTime(): String {
|
|
||||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
|
||||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
return df.format(Date())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runCommand(command: String): String {
|
|
||||||
val byteOut = ByteArrayOutputStream()
|
|
||||||
project.exec {
|
|
||||||
commandLine = command.split(" ")
|
|
||||||
standardOutput = byteOut
|
|
||||||
}
|
|
||||||
return String(byteOut.toByteArray()).trim()
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
-allowaccessmodification
|
|
||||||
-dontusemixedcaseclassnames
|
|
||||||
-verbose
|
|
||||||
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
|
||||||
|
|
||||||
-keep @androidx.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@androidx.annotation.Keep <init>(...);
|
|
||||||
}
|
|
226
app/proguard-rules.pro
vendored
@ -1,20 +1,37 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
# Keep extension's common dependencies
|
# Extensions may require methods unused in the core app
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
-dontwarn eu.kanade.tachiyomi.**
|
||||||
-keep,allowoptimization class androidx.preference.** { *; }
|
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
|
||||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
|
||||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
|
||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
-keep class org.jsoup.** { *; }
|
||||||
|
-keep class kotlin.** { *; }
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep class com.google.gson.** { *; }
|
||||||
|
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||||
|
-keep class com.squareup.duktape.** { *; }
|
||||||
|
|
||||||
|
# === Keep EH classes
|
||||||
|
-keep class exh.** { *; }
|
||||||
|
-keep class xyz.nulldev.** { *; }
|
||||||
|
|
||||||
|
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
|
||||||
|
-keep class rx.android.** { *; }
|
||||||
|
|
||||||
|
# Design library
|
||||||
|
-dontwarn com.google.android.material.**
|
||||||
|
-keep class com.google.android.material.** { *; }
|
||||||
|
-keep interface com.google.android.material.** { *; }
|
||||||
|
-keep public class com.google.android.material.R$* { *; }
|
||||||
|
|
||||||
|
-keep class com.hippo.image.** { *; }
|
||||||
|
-keep interface com.hippo.image.** { *; }
|
||||||
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||||
@ -31,9 +48,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
-dontnote rx.internal.util.PlatformDependent
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
##---------------End: proguard configuration for RxJava 1.x ----------
|
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for Gson ----------
|
# === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration
|
||||||
|
-dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||||
|
-dontwarn io.reactivex.functions.Function
|
||||||
|
-dontwarn rx.internal.util.**
|
||||||
|
-dontwarn sun.misc.Unsafe
|
||||||
|
|
||||||
|
# === Okhttp: https://github.com/square/okhttp/blob/3637fc56f70f87da696847defd311dbfb28e87b5/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro
|
||||||
|
# JSR 305 annotations are for embedding nullability information.
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||||
|
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||||
|
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||||
|
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||||
|
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||||
|
|
||||||
|
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
# === GSON: https://raw.githubusercontent.com/google/gson/master/examples/android-proguard-example/proguard.cfg
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
# removes such information by default, so configure it to keep all of it.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
@ -43,10 +78,13 @@
|
|||||||
|
|
||||||
# Gson specific classes
|
# Gson specific classes
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
#-keep class com.google.gson.stream.** { *; }
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
# Application classes that will be serialized/deserialized over Gson
|
||||||
|
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||||
|
|
||||||
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
-keep class * extends com.google.gson.TypeAdapter
|
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
@ -55,30 +93,140 @@
|
|||||||
-keepclassmembers,allowobfuscation class * {
|
-keepclassmembers,allowobfuscation class * {
|
||||||
@com.google.gson.annotations.SerializedName <fields>;
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
}
|
}
|
||||||
##---------------End: proguard configuration for Gson ----------
|
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
# == Nucleus
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
<init>();
|
||||||
|
|
||||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
|
||||||
*** Companion;
|
|
||||||
}
|
|
||||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
# TODO Changeloglib? Does it need proguard?
|
||||||
-keepclassmembers class eu.kanade.tachiyomi.** {
|
|
||||||
*** Companion;
|
# === Injekt
|
||||||
}
|
## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1"
|
||||||
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
-keep class uy.kohesive.injekt.** { *; }
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
|
||||||
|
|
||||||
|
# === Glide
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class kotlinx.serialization.**
|
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
|
||||||
-keepclassmembers class kotlinx.serialization.** {
|
|
||||||
<methods>;
|
# === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt
|
||||||
|
-dontwarn jp.co.cyberagent.android.gpuimage.**
|
||||||
|
|
||||||
|
# === Conductor
|
||||||
|
# This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files
|
||||||
|
-keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler {
|
||||||
|
public <init>();
|
||||||
|
}
|
||||||
|
|
||||||
|
# === RxBinding
|
||||||
|
-dontwarn com.google.auto.value.AutoValue
|
||||||
|
|
||||||
|
# === Crashlytics
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
-keep class com.crashlytics.** { *; }
|
||||||
|
-dontwarn com.crashlytics.**
|
||||||
|
|
||||||
|
# === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-keep class org.ocpsoft.prettytime.i18n.**
|
||||||
|
|
||||||
|
# Note: We intentionally don't add the flags we'd need to make Enums work.
|
||||||
|
# That's because the Proguard configuration required to make it work on
|
||||||
|
# optimized code would preclude lots of optimization, like converting enums
|
||||||
|
# into ints.
|
||||||
|
|
||||||
|
# Throwables uses internal APIs for lazy stack trace resolution
|
||||||
|
-dontnote sun.misc.SharedSecrets
|
||||||
|
-keep class sun.misc.SharedSecrets {
|
||||||
|
*** getJavaLangAccess(...);
|
||||||
|
}
|
||||||
|
-dontnote sun.misc.JavaLangAccess
|
||||||
|
-keep class sun.misc.JavaLangAccess {
|
||||||
|
*** getStackTraceElement(...);
|
||||||
|
*** getStackTraceDepth(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# FinalizableReferenceQueue calls this reflectively
|
||||||
|
# Proguard is intelligent enough to spot the use of reflection onto this, so we
|
||||||
|
# only need to keep the names, and allow it to be stripped out if
|
||||||
|
# FinalizableReferenceQueue is unused.
|
||||||
|
-keepnames class com.google.common.base.internal.Finalizer {
|
||||||
|
*** startFinalizer(...);
|
||||||
|
}
|
||||||
|
# However, it cannot "spot" that this method needs to be kept IF the class is.
|
||||||
|
-keepclassmembers class com.google.common.base.internal.Finalizer {
|
||||||
|
*** startFinalizer(...);
|
||||||
|
}
|
||||||
|
-keepnames class com.google.common.base.FinalizableReference {
|
||||||
|
void finalizeReferent();
|
||||||
|
}
|
||||||
|
-keepclassmembers class com.google.common.base.FinalizableReference {
|
||||||
|
void finalizeReferent();
|
||||||
|
}
|
||||||
|
|
||||||
|
# Striped64, LittleEndianByteArray, UnsignedBytes, AbstractFuture
|
||||||
|
-dontwarn sun.misc.Unsafe
|
||||||
|
|
||||||
|
# Striped64 appears to make some assumptions about object layout that
|
||||||
|
# really might not be safe. This should be investigated.
|
||||||
|
-keepclassmembers class com.google.common.cache.Striped64 {
|
||||||
|
*** base;
|
||||||
|
*** busy;
|
||||||
|
}
|
||||||
|
-keepclassmembers class com.google.common.cache.Striped64$Cell {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn java.lang.SafeVarargs
|
||||||
|
|
||||||
|
-keep class java.lang.Throwable {
|
||||||
|
*** addSuppressed(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Futures.getChecked, in both of its variants, is incompatible with proguard.
|
||||||
|
|
||||||
|
# Used by AtomicReferenceFieldUpdater and sun.misc.Unsafe
|
||||||
|
-keepclassmembers class com.google.common.util.concurrent.AbstractFuture** {
|
||||||
|
*** waiters;
|
||||||
|
*** value;
|
||||||
|
*** listeners;
|
||||||
|
*** thread;
|
||||||
|
*** next;
|
||||||
|
}
|
||||||
|
-keepclassmembers class com.google.common.util.concurrent.AtomicDouble {
|
||||||
|
*** value;
|
||||||
|
}
|
||||||
|
-keepclassmembers class com.google.common.util.concurrent.AggregateFutureState {
|
||||||
|
*** remaining;
|
||||||
|
*** seenExceptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Since Unsafe is using the field offsets of these inner classes, we don't want
|
||||||
|
# to have class merging or similar tricks applied to these classes and their
|
||||||
|
# fields. It's safe to allow obfuscation, since the by-name references are
|
||||||
|
# already preserved in the -keep statement above.
|
||||||
|
-keep,allowshrinking,allowobfuscation class com.google.common.util.concurrent.AbstractFuture** {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Futures.getChecked (which often won't work with Proguard anyway) uses this. It
|
||||||
|
# has a fallback, but again, don't use Futures.getChecked on Android regardless.
|
||||||
|
-dontwarn java.lang.ClassValue
|
||||||
|
|
||||||
|
# MoreExecutors references AppEngine
|
||||||
|
-dontnote com.google.appengine.api.ThreadManager
|
||||||
|
-keep class com.google.appengine.api.ThreadManager {
|
||||||
|
static *** currentRequestThreadFactory(...);
|
||||||
|
}
|
||||||
|
-dontnote com.google.apphosting.api.ApiProxy
|
||||||
|
-keep class com.google.apphosting.api.ApiProxy {
|
||||||
|
static *** getCurrentEnvironment (...);
|
||||||
}
|
}
|
||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
android:shortcutShortLabel="@string/label_recent_updates">
|
android:shortcutShortLabel="@string/short_recent_updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
@ -5,23 +5,93 @@
|
|||||||
android:viewportHeight="108.0">
|
android:viewportHeight="108.0">
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#000"/>
|
android:fillColor="#000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#455A64"/>
|
android:fillColor="#455A64"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
||||||
|
android:fillType="evenOdd"
|
||||||
android:fillColor="#607D8B"/>
|
android:fillColor="#607D8B"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
android:name="path_3"
|
||||||
android:fillColor="#000"/>
|
android:pathData="M 54 54.5 M 28.5 54.5 C 28.5 47.74 31.188 41.249 35.969 36.469 C 40.749 31.688 47.24 29 54 29 C 60.76 29 67.251 31.688 72.031 36.469 C 76.812 41.249 79.5 47.74 79.5 54.5 C 79.5 61.26 76.812 67.751 72.031 72.531 C 67.251 77.312 60.76 80 54 80 C 47.24 80 40.749 77.312 35.969 72.531 C 31.188 67.751 28.5 61.26 28.5 54.5"
|
||||||
|
android:fillColor="#000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
android:name="path_4"
|
||||||
android:fillColor="#CE2828"/>
|
android:pathData="M 54 54.5 M 28.5 54.5 C 28.5 47.74 31.188 41.249 35.969 36.469 C 40.749 31.688 47.24 29 54 29 C 60.76 29 67.251 31.688 72.031 36.469 C 76.812 41.249 79.5 47.74 79.5 54.5 C 79.5 61.26 76.812 67.751 72.031 72.531 C 67.251 77.312 60.76 80 54 80 C 47.24 80 40.749 77.312 35.969 72.531 C 31.188 67.751 28.5 61.26 28.5 54.5"
|
||||||
|
android:fillColor="#CE2828"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
android:name="path_5"
|
||||||
android:fillColor="#FFF"/>
|
android:pathData="M 54 54.5 M 34.06 54.5 C 33.964 50.23 35.243 46.04 37.707 42.551 C 40.171 39.062 43.692 36.455 47.748 35.117 C 51.805 33.779 56.185 33.779 60.242 35.117 C 64.298 36.455 67.819 39.062 70.283 42.551 C 72.747 46.04 74.026 50.23 73.93 54.5 C 74.026 58.77 72.747 62.96 70.283 66.449 C 67.819 69.938 64.298 72.545 60.242 73.883 C 56.185 75.221 51.805 75.221 47.748 73.883 C 43.692 72.545 40.171 69.938 37.707 66.449 C 35.243 62.96 33.964 58.77 34.06 54.5"
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
android:name="path_6"
|
||||||
android:fillColor="#000"/>
|
android:pathData="M 54.174 36.266 C 64.147 36.266 72.234 44.397 72.234 54.426 C 72.234 64.459 64.147 72.593 54.174 72.593 C 44.197 72.593 36.113 64.459 36.113 54.426 C 36.113 44.397 44.197 36.266 54.174 36.266 Z"
|
||||||
|
android:fillColor="#ffcc4d"
|
||||||
|
android:strokeColor="#ffcc4d"
|
||||||
|
android:strokeWidth="4.628571428571428"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"/>
|
||||||
|
<path
|
||||||
|
android:name="path_7"
|
||||||
|
android:pathData="M 48.774 45.158 C 49.988 45.158 50.973 46.452 50.973 48.05 C 50.973 49.65 49.988 50.946 48.774 50.946 C 47.559 50.946 46.576 49.65 46.576 48.05 C 46.576 46.452 47.559 45.158 48.774 45.158 Z"
|
||||||
|
android:fillColor="#674600"
|
||||||
|
android:strokeColor="#674600"
|
||||||
|
android:strokeWidth="0.1"
|
||||||
|
android:strokeLineJoin="round"/>
|
||||||
|
<path
|
||||||
|
android:name="path_8"
|
||||||
|
android:pathData="M 62.02 45.158 C 63.235 45.158 64.219 46.452 64.219 48.05 C 64.219 49.65 63.235 50.946 62.02 50.946 C 60.805 50.946 59.821 49.65 59.821 48.05 C 59.821 46.452 60.805 45.158 62.02 45.158 Z"
|
||||||
|
android:fillColor="#674600"
|
||||||
|
android:strokeColor="#674600"
|
||||||
|
android:strokeWidth="0.1"
|
||||||
|
android:strokeLineJoin="round"/>
|
||||||
|
<path
|
||||||
|
android:name="path_9"
|
||||||
|
android:pathData="M 44.687 42.102 C 44.687 42.102 48.404 40.049 53.119 42.102"
|
||||||
|
android:strokeColor="#674600"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:name="path_10"
|
||||||
|
android:pathData="M 58.035 42.102 C 58.035 42.102 61.751 40.049 66.465 42.102"
|
||||||
|
android:strokeColor="#674600"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:name="path_11"
|
||||||
|
android:pathData="M 49.191 56.685 C 49.191 56.685 51.703 59.685 56.992 59.685"
|
||||||
|
android:strokeColor="#674600"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:name="path_12"
|
||||||
|
android:pathData="M 34.072 67.511 C 34.072 67.511 34.524 66.248 37.476 63.634 C 37.476 63.634 39.143 61.991 37.876 58.114 C 37.876 58.114 37.534 56.06 39.681 57.255 C 39.681 57.255 44.129 60.46 40.871 65 C 40.871 65 40.241 66.117 41.652 65.78 L 53.939 63.103 C 53.939 63.103 55.275 62.752 55.535 64.163 C 55.535 64.163 55.696 65.372 54.641 65.672 L 47.742 67.378 C 47.742 67.378 50.72 69.597 47.742 70.825 C 47.742 70.825 50.294 72.779 47.336 74.268 C 47.336 74.268 49.276 76.015 46.407 77.019 C 46.407 77.019 42.525 78.474 39.209 77.831 C 35.509 77.103 31.947 72.114 34.072 67.511 Z"
|
||||||
|
android:fillColor="#f4900c"
|
||||||
|
android:strokeColor="#f4900c"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="0.1"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:name="path_13"
|
||||||
|
android:pathData="M 56.182 67.511 C 56.182 67.511 56.633 66.248 59.585 63.634 C 59.585 63.634 61.253 61.991 59.985 58.114 C 59.985 58.114 59.642 56.06 61.789 57.255 C 61.789 57.255 66.239 60.46 62.98 65 C 62.98 65 62.351 66.117 63.761 65.78 L 76.066 63.103 C 76.066 63.103 77.386 62.752 77.641 64.163 C 77.641 64.163 77.812 65.372 76.746 65.672 L 69.85 67.378 C 69.85 67.378 72.829 69.597 69.85 70.825 C 69.85 70.825 72.404 72.779 69.446 74.268 C 69.446 74.268 71.387 76.015 68.516 77.019 C 68.516 77.019 64.633 78.474 61.317 77.831 C 57.618 77.103 54.057 72.114 56.182 67.511 Z"
|
||||||
|
android:fillColor="#f4900c"
|
||||||
|
android:strokeColor="#f4900c"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="0.1"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
6
app/src/debug/res/drawable/ic_migrate_direction.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<vector android:height="100dp"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 -5,5z"/>
|
||||||
|
</vector>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
263
app/src/main/AndroidManifest.xml
Normal file → Executable file
@ -1,29 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<!-- Internet -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Storage -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<!-- For background jobs -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<!-- For managing extensions -->
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
|
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<!-- Lock vibrate -->
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@ -31,15 +33,13 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:theme="@style/Theme.Tachiyomi.Light"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
android:theme="@style/Theme.Splash">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -49,12 +49,15 @@
|
|||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.main.ForceCloseActivity"
|
||||||
|
android:clearTaskOnLaunch="true"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
android:label="@string/action_global_search"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -65,48 +68,27 @@
|
|||||||
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask" />
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
|
||||||
android:resource="@xml/s_pen_actions"/>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.BiometricUnlockActivity"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
android:theme="@style/Theme.Splash" />
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize"
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:label="@string/app_name"
|
||||||
android:exported="false" />
|
android:theme="@style/FilePickerTheme" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist"
|
android:label="Anilist">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -118,25 +100,9 @@
|
|||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
|
||||||
android:label="MyAnimeList"
|
|
||||||
android:exported="true">
|
|
||||||
<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:host="myanimelist-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori"
|
android:label="Shikimori">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -150,8 +116,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi"
|
android:label="Bangumi">
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -164,6 +129,27 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||||
|
android:theme="@style/Theme.MaterialComponents" />
|
||||||
|
<activity
|
||||||
|
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||||
|
android:theme="@style/Theme.MaterialComponents" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -188,16 +174,139 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<!-- EH -->
|
||||||
android:name="androidx.core.content.FileProvider"
|
<service
|
||||||
android:authorities="${applicationId}.provider"
|
android:name="exh.eh.EHentaiUpdateWorker"
|
||||||
android:exported="false"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
android:grantUriPermissions="true">
|
android:exported="true" />
|
||||||
<meta-data
|
<activity
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="exh.ui.intercept.InterceptActivity"
|
||||||
android:resource="@xml/provider_paths" />
|
android:label="TachiyomiEH"
|
||||||
</provider>
|
android:theme="@style/Theme.EHActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<!-- EH -->
|
||||||
|
<data
|
||||||
|
android:host="g.e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="g.e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="e-hentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- EXH -->
|
||||||
|
<data
|
||||||
|
android:host="exhentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="exhentai.org"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- nhentai -->
|
||||||
|
<data
|
||||||
|
android:host="nhentai.net"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="nhentai.net"
|
||||||
|
android:pathPrefix="/g/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Perv Eden -->
|
||||||
|
<data
|
||||||
|
android:host="www.perveden.com"
|
||||||
|
android:pathPattern="/.*/.*-manga/.*"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.perveden.com"
|
||||||
|
android:pathPattern="/.*/.*-manga/.*"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Hentai Cafe -->
|
||||||
|
<data
|
||||||
|
android:host="hentai.cafe"
|
||||||
|
android:pathPattern="/.*/.*"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hentai.cafe"
|
||||||
|
android:pathPattern="/.*/.*"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Tsumino -->
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Book/Info/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Book/Info/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Read/View/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.tsumino.com"
|
||||||
|
android:pathPrefix="/Read/View/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Hitomi.la -->
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/galleries/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/reader/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/galleries/"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="hitomi.la"
|
||||||
|
android:pathPrefix="/reader/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- Pururin.io -->
|
||||||
|
<data
|
||||||
|
android:host="pururin.io"
|
||||||
|
android:pathPrefix="/gallery/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="pururin.io"
|
||||||
|
android:pathPrefix="/gallery/"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<!-- HBrowse -->
|
||||||
|
<data
|
||||||
|
android:host="www.hbrowse.com"
|
||||||
|
android:pathPrefix="/"
|
||||||
|
android:scheme="http" />
|
||||||
|
<data
|
||||||
|
android:host="www.hbrowse.com"
|
||||||
|
android:pathPrefix="/"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
|
android:theme="@style/Theme.EHActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
0
app/src/main/ic_launcher-web.png
Normal file → Executable file
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
291
app/src/main/java/eu/kanade/tachiyomi/App.kt
Normal file → Executable file
@ -1,183 +1,216 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.res.Configuration
|
||||||
import android.content.IntentFilter
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.webkit.WebView
|
import android.os.Environment
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.multidex.MultiDex
|
||||||
import coil.ImageLoader
|
import com.elvishew.xlog.LogConfiguration
|
||||||
import coil.ImageLoaderFactory
|
import com.elvishew.xlog.LogLevel
|
||||||
import coil.decode.GifDecoder
|
import com.elvishew.xlog.XLog
|
||||||
import coil.decode.ImageDecoderDecoder
|
import com.elvishew.xlog.printer.AndroidPrinter
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import com.elvishew.xlog.printer.Printer
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import com.elvishew.xlog.printer.file.FilePrinter
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
||||||
|
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
||||||
|
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||||
|
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||||
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
|
import com.kizitonwose.time.days
|
||||||
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
import eu.kanade.tachiyomi.ui.main.ForceCloseActivity
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import kotlinx.coroutines.flow.onEach
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import org.acra.config.httpSender
|
import exh.debug.DebugToggles
|
||||||
import org.acra.ktx.initAcra
|
import exh.log.CrashlyticsPrinter
|
||||||
import org.acra.sender.HttpSender
|
import exh.log.EHDebugModeOverlay
|
||||||
import org.conscrypt.Conscrypt
|
import exh.log.EHLogLevel
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import java.io.File
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.Security
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
setupExhLogging() // EXH logging
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
workaroundAndroid7BrokenSSL()
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
// Enforce WebView availability
|
||||||
}
|
if (!WebViewUtil.supportsWebView(this)) {
|
||||||
|
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||||
// Avoid potential crashes
|
ForceCloseActivity.closeApp(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
val process = getProcessName()
|
|
||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupAcra()
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
|
||||||
|
// Reprint.initialize(this) //Setup fingerprint (EH)
|
||||||
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
|
setupDebugOverlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
|
||||||
preferences.incognitoMode().asFlow()
|
|
||||||
.onEach { enabled ->
|
|
||||||
val notificationManager = NotificationManagerCompat.from(this)
|
|
||||||
if (enabled) {
|
|
||||||
disableIncognitoReceiver.register()
|
|
||||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
|
||||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
|
||||||
setContentText(getString(R.string.notification_incognito_text))
|
|
||||||
setSmallIcon(R.drawable.ic_glasses_24dp)
|
|
||||||
setOngoing(true)
|
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@App,
|
|
||||||
0,
|
|
||||||
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
|
||||||
PendingIntent.FLAG_ONE_SHOT
|
|
||||||
)
|
|
||||||
setContentIntent(pendingIntent)
|
|
||||||
}
|
|
||||||
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
|
||||||
} else {
|
|
||||||
disableIncognitoReceiver.unregister()
|
|
||||||
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
|
||||||
|
|
||||||
preferences.themeMode()
|
|
||||||
.asImmediateFlow {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
|
||||||
when (it) {
|
|
||||||
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun attachBaseContext(base: Context) {
|
||||||
return ImageLoader.Builder(this).apply {
|
super.attachBaseContext(base)
|
||||||
componentRegistry {
|
MultiDex.install(this)
|
||||||
add(TachiyomiImageDecoder(this@App.resources))
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
add(ImageDecoderDecoder(this@App))
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
} else {
|
super.onConfigurationChanged(newConfig)
|
||||||
add(GifDecoder())
|
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun workaroundAndroid7BrokenSSL() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
||||||
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
SSLContext.getInstance("TLSv1.2")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
XLog.e("Could not install Android 7 broken SSL workaround!", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ProviderInstaller.installIfNeeded(applicationContext)
|
||||||
|
} catch (e: GooglePlayServicesRepairableException) {
|
||||||
|
XLog.e("Could not install Android 7 broken SSL workaround!", e)
|
||||||
|
} catch (e: GooglePlayServicesNotAvailableException) {
|
||||||
|
XLog.e("Could not install Android 7 broken SSL workaround!", e)
|
||||||
}
|
}
|
||||||
add(ByteBufferFetcher())
|
|
||||||
add(MangaCoverFetcher())
|
|
||||||
}
|
}
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
|
||||||
crossfade(300)
|
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
|
||||||
}.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun onAppBackgrounded() {
|
fun onAppBackgrounded() {
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
if (preferences.lockAppAfter().get() >= 0) {
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
|
||||||
if (BuildConfig.FLAVOR != "dev") {
|
|
||||||
initAcra {
|
|
||||||
buildConfigClass = BuildConfig::class.java
|
|
||||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
|
||||||
|
|
||||||
httpSender {
|
|
||||||
uri = BuildConfig.ACRA_URI
|
|
||||||
httpMethod = HttpSender.Method.PUT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
// EXH
|
||||||
private var registered = false
|
private fun deleteOldMetadataRealm() {
|
||||||
|
Realm.init(this)
|
||||||
|
val config = RealmConfiguration.Builder()
|
||||||
|
.name("gallery-metadata.realm")
|
||||||
|
.schemaVersion(3)
|
||||||
|
.deleteRealmIfMigrationNeeded()
|
||||||
|
.build()
|
||||||
|
Realm.deleteRealm(config)
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
// Delete old paper db files
|
||||||
preferences.incognitoMode().set(false)
|
listOf(
|
||||||
|
File(filesDir, "gallery-ex"),
|
||||||
|
File(filesDir, "gallery-perveden"),
|
||||||
|
File(filesDir, "gallery-nhentai")
|
||||||
|
).forEach {
|
||||||
|
if (it.exists()) {
|
||||||
|
thread {
|
||||||
|
it.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun register() {
|
|
||||||
if (!registered) {
|
|
||||||
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
|
||||||
registered = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregister() {
|
|
||||||
if (registered) {
|
|
||||||
unregisterReceiver(this)
|
|
||||||
registered = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
// EXH
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
private fun setupExhLogging() {
|
||||||
|
EHLogLevel.init(this)
|
||||||
|
|
||||||
|
val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) {
|
||||||
|
LogLevel.ALL
|
||||||
|
} else {
|
||||||
|
LogLevel.WARN
|
||||||
|
}
|
||||||
|
|
||||||
|
val logConfig = LogConfiguration.Builder()
|
||||||
|
.logLevel(logLevel)
|
||||||
|
.t()
|
||||||
|
.st(2)
|
||||||
|
.nb()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val printers = mutableListOf<Printer>(AndroidPrinter())
|
||||||
|
|
||||||
|
val logFolder = File(
|
||||||
|
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
||||||
|
getString(R.string.app_name),
|
||||||
|
"logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
printers += FilePrinter
|
||||||
|
.Builder(logFolder.absolutePath)
|
||||||
|
.fileNameGenerator(object : DateFileNameGenerator() {
|
||||||
|
override fun generateFileName(logLevel: Int, timestamp: Long): String {
|
||||||
|
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue))
|
||||||
|
.backupStrategy(NeverBackupStrategy())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Install Crashlytics in prod
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
printers += CrashlyticsPrinter(LogLevel.ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
XLog.init(
|
||||||
|
logConfig,
|
||||||
|
*printers.toTypedArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
XLog.d("Application booting...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXH
|
||||||
|
private fun setupDebugOverlay() {
|
||||||
|
try {
|
||||||
|
DebugOverlay.Builder(this)
|
||||||
|
.modules(FpsModule(), EHDebugModeOverlay(this))
|
||||||
|
.bgColor(Color.parseColor("#7F000000"))
|
||||||
|
.notification(false)
|
||||||
|
.allowSystemLayer(false)
|
||||||
|
.build()
|
||||||
|
.install()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
// Crashes if app is in background
|
||||||
|
XLog.e("Failed to initialize debug overlay, app in background?", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.content.ContextCompat
|
import com.google.gson.Gson
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -11,7 +11,10 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import kotlinx.serialization.json.Json
|
import exh.eh.EHentaiUpdateHelper
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@ -41,19 +44,22 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
addSingletonFactory { EHentaiUpdateHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { Markwon.create(app) }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
|
||||||
get<PreferencesHelper>()
|
|
||||||
|
|
||||||
get<NetworkHelper>()
|
GlobalScope.launch { get<PreferencesHelper>() }
|
||||||
|
|
||||||
get<SourceManager>()
|
GlobalScope.launch { get<NetworkHelper>() }
|
||||||
|
|
||||||
get<DatabaseHelper>()
|
GlobalScope.launch { get<SourceManager>() }
|
||||||
|
|
||||||
get<DownloadManager>()
|
GlobalScope.launch { get<DatabaseHelper>() }
|
||||||
}
|
|
||||||
|
GlobalScope.launch { get<DownloadManager>() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
|
|
||||||
|
// TODO NATIVE TACHIYOMI MIGRATIONS ARE FUCKED UP DUE TO DIFFERING VERSION NUMBERS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a migration when the application is updated.
|
* Performs a migration when the application is updated.
|
||||||
*
|
*
|
||||||
@ -31,13 +20,13 @@ object Migrations {
|
|||||||
*/
|
*/
|
||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
|
|
||||||
// Cancel app updater job for debug builds that don't include it
|
// Cancel app updater job for debug builds that don't include it
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.cancelTask(context)
|
UpdaterJob.cancelTask(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
@ -98,148 +87,12 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
|
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (oldSortingMode == LibrarySort.SOURCE) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 52) {
|
|
||||||
// Migrate library filters to tri-state versions
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
|
||||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
|
||||||
else ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value
|
|
||||||
}
|
|
||||||
prefs.edit {
|
|
||||||
putInt(PreferenceKeys.filterDownloaded, convertBooleanPrefToTriState("pref_filter_downloaded_key"))
|
|
||||||
remove("pref_filter_downloaded_key")
|
|
||||||
|
|
||||||
putInt(PreferenceKeys.filterUnread, convertBooleanPrefToTriState("pref_filter_unread_key"))
|
|
||||||
remove("pref_filter_unread_key")
|
|
||||||
|
|
||||||
putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key"))
|
|
||||||
remove("pref_filter_completed_key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 54) {
|
|
||||||
// Force MAL log out due to login flow change
|
|
||||||
// v52: switched from scraping to WebView
|
|
||||||
// v53: switched from WebView to OAuth
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
|
||||||
if (trackManager.myAnimeList.isLogged) {
|
|
||||||
trackManager.myAnimeList.logout()
|
|
||||||
context.toast(R.string.myanimelist_relogin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 57) {
|
|
||||||
// Migrate DNS over HTTPS setting
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
|
||||||
if (wasDohEnabled) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
|
||||||
remove("enable_doh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 59) {
|
|
||||||
// Reset rotation to Free after replacing Lock
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_rotation_type_key", 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable update check for Android 5.x users
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
|
||||||
UpdaterJob.cancelTask(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 60) {
|
|
||||||
// Re-enable update check that was prevously accidentally disabled for M
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
|
||||||
UpdaterJob.setupTask(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
|
||||||
1 -> OrientationType.FREE.flagValue
|
|
||||||
2 -> OrientationType.PORTRAIT.flagValue
|
|
||||||
3 -> OrientationType.LANDSCAPE.flagValue
|
|
||||||
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
|
||||||
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
|
||||||
else -> OrientationType.FREE.flagValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reading mode flag and prefValue is the same value
|
|
||||||
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putInt("pref_default_orientation_type_key", newOrientation)
|
|
||||||
remove("pref_rotation_type_key")
|
|
||||||
putInt("pref_default_reading_mode_key", newReadingMode)
|
|
||||||
remove("pref_default_viewer_key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 61) {
|
|
||||||
// Handle removed every 1 or 2 hour library updates
|
|
||||||
val updateInterval = preferences.libraryUpdateInterval().get()
|
|
||||||
if (updateInterval == 1 || updateInterval == 2) {
|
|
||||||
preferences.libraryUpdateInterval().set(3)
|
|
||||||
LibraryUpdateJob.setupTask(context, 3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 64) {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
|
||||||
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val newSortingMode = when (oldSortingMode) {
|
|
||||||
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
|
||||||
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
|
||||||
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
|
||||||
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
|
||||||
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
|
||||||
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
|
||||||
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
|
||||||
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
|
||||||
else -> SortModeSetting.ALPHABETICAL
|
|
||||||
}
|
|
||||||
|
|
||||||
val newSortingDirection = when (oldSortingDirection) {
|
|
||||||
true -> SortDirectionSetting.ASCENDING
|
|
||||||
else -> SortDirectionSetting.DESCENDING
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit(commit = true) {
|
|
||||||
remove(PreferenceKeys.librarySortingMode)
|
|
||||||
remove(PreferenceKeys.librarySortingDirection)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putString(PreferenceKeys.librarySortingMode, newSortingMode.name)
|
|
||||||
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldVersion < 65) {
|
|
||||||
if (preferences.lang().get() in listOf("en-US", "en-GB")) {
|
|
||||||
preferences.lang().set("en")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Nsfw
|
|
@ -1,96 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class AbstractBackupManager(protected val context: Context) {
|
|
||||||
|
|
||||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
|
||||||
internal val sourceManager: SourceManager by injectLazy()
|
|
||||||
internal val trackManager: TrackManager by injectLazy()
|
|
||||||
protected val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns manga
|
|
||||||
*
|
|
||||||
* @return [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
|
||||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches chapter information.
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters list of chapters in the backup
|
|
||||||
* @return Updated manga chapters.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
|
||||||
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
|
|
||||||
.map { it.toSChapter() }
|
|
||||||
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source)
|
|
||||||
if (syncedChapters.first.isNotEmpty()) {
|
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
|
||||||
updateChapters(chapters)
|
|
||||||
}
|
|
||||||
return syncedChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns list containing manga from library
|
|
||||||
*
|
|
||||||
* @return [Manga] from library
|
|
||||||
*/
|
|
||||||
protected fun getFavoriteManga(): List<Manga> =
|
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal fun insertManga(manga: Manga): Long? =
|
|
||||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
protected fun insertChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.insertChapters(chapters).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a list of chapters
|
|
||||||
*/
|
|
||||||
protected fun updateChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a list of chapters with known database ids
|
|
||||||
*/
|
|
||||||
protected fun updateKnownChapters(chapters: List<Chapter>) {
|
|
||||||
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return number of backups.
|
|
||||||
*
|
|
||||||
* @return number of backups selected by user
|
|
||||||
*/
|
|
||||||
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
|
|
||||||
|
|
||||||
protected val db: DatabaseHelper by injectLazy()
|
|
||||||
protected val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
var job: Job? = null
|
|
||||||
|
|
||||||
protected lateinit var backupManager: T
|
|
||||||
|
|
||||||
protected var restoreAmount = 0
|
|
||||||
protected var restoreProgress = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of source ID to source name from backup data
|
|
||||||
*/
|
|
||||||
protected var sourceMapping: Map<Long, String> = emptyMap()
|
|
||||||
|
|
||||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
|
||||||
|
|
||||||
abstract suspend fun performRestore(uri: Uri): Boolean
|
|
||||||
|
|
||||||
suspend fun restoreBackup(uri: Uri): Boolean {
|
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
restoreProgress = 0
|
|
||||||
errors.clear()
|
|
||||||
|
|
||||||
if (!performRestore(uri)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val endTime = System.currentTimeMillis()
|
|
||||||
val time = endTime - startTime
|
|
||||||
|
|
||||||
val logFile = writeErrorLog()
|
|
||||||
|
|
||||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches chapter information.
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga chapters.
|
|
||||||
*/
|
|
||||||
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
|
||||||
return try {
|
|
||||||
backupManager.restoreChapters(source, manga, chapters)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If there's any error, return empty update and continue.
|
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
|
||||||
context.getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
|
||||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
|
||||||
Pair(emptyList(), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes tracking information.
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating.
|
|
||||||
* @param tracks list containing tracks from restore file.
|
|
||||||
*/
|
|
||||||
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
|
|
||||||
tracks.forEach { track ->
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
try {
|
|
||||||
val updatedTrack = service.refresh(track)
|
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val serviceName = service?.nameRes()?.let { context.getString(it) }
|
|
||||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to update dialog in [BackupConst]
|
|
||||||
*
|
|
||||||
* @param progress restore progress
|
|
||||||
* @param amount total restoreAmount of manga
|
|
||||||
* @param title title of restored manga
|
|
||||||
*/
|
|
||||||
internal fun showRestoreProgress(
|
|
||||||
progress: Int,
|
|
||||||
amount: Int,
|
|
||||||
title: String
|
|
||||||
) {
|
|
||||||
notifier.showRestoreProgress(title, progress, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun writeErrorLog(): File {
|
|
||||||
try {
|
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
file.bufferedWriter().use { out ->
|
|
||||||
errors.forEach { (date, message) ->
|
|
||||||
out.write("[${sdf.format(date)}] $message\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Empty
|
|
||||||
}
|
|
||||||
return File("")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class AbstractBackupRestoreValidator {
|
|
||||||
protected val sourceManager: SourceManager by injectLazy()
|
|
||||||
protected val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
abstract fun validate(context: Context, uri: Uri): Results
|
|
||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
|
||||||
}
|
|
@ -7,8 +7,4 @@ object BackupConst {
|
|||||||
private const val NAME = "BackupRestoreServices"
|
private const val NAME = "BackupRestoreServices"
|
||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
|
||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
|
||||||
const val BACKUP_TYPE_FULL = 1
|
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,10 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@ -53,7 +51,11 @@ class BackupCreateService : Service() {
|
|||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,6 +65,7 @@ class BackupCreateService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -99,9 +102,11 @@ class BackupCreateService : Service() {
|
|||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
backupManager = BackupManager(this)
|
||||||
|
|
||||||
|
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile)
|
notifier.showBackupComplete(unifile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import android.net.Uri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val backupManager = BackupManager(context)
|
||||||
|
val uri = Uri.parse(preferences.backupsDirectory().get())
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
backupManager.createBackup(uri, flags, true)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure()
|
Result.failure()
|
||||||
@ -36,10 +36,8 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(),
|
interval.toLong(), TimeUnit.HOURS,
|
||||||
TimeUnit.HOURS,
|
10, TimeUnit.MINUTES
|
||||||
10,
|
|
||||||
TimeUnit.MINUTES
|
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.build()
|
.build()
|
||||||
|
532
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt
Executable file
@ -0,0 +1,532 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeAdapter
|
||||||
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import exh.eh.EHentaiThrottleManager
|
||||||
|
import kotlin.math.max
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||||
|
|
||||||
|
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||||
|
internal val sourceManager: SourceManager by injectLazy()
|
||||||
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of parser
|
||||||
|
*/
|
||||||
|
var version: Int = version
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json Parser
|
||||||
|
*/
|
||||||
|
var parser: Gson = initParser()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set version of parser
|
||||||
|
*
|
||||||
|
* @param version version of parser
|
||||||
|
*/
|
||||||
|
internal fun setVersion(version: Int) {
|
||||||
|
this.version = version
|
||||||
|
parser = initParser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initParser(): Gson = when (version) {
|
||||||
|
1 -> GsonBuilder().create()
|
||||||
|
2 ->
|
||||||
|
GsonBuilder()
|
||||||
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
.create()
|
||||||
|
else -> throw Exception("Json version unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||||
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create extension ID/name mapping
|
||||||
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
|
root[Backup.MANGAS] = mangaEntries
|
||||||
|
root[CATEGORIES] = categoryEntries
|
||||||
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
|
||||||
|
databaseHelper.inTransaction {
|
||||||
|
// Get manga from database
|
||||||
|
val mangas = getFavoriteManga()
|
||||||
|
|
||||||
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
|
// Backup library manga and its dependencies
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
mangaEntries.add(backupMangaObject(manga, flags))
|
||||||
|
|
||||||
|
// Maintain set of extensions/sources used (excludes local source)
|
||||||
|
if (manga.source != LocalSource.ID) {
|
||||||
|
sourceManager.get(manga.source)?.let {
|
||||||
|
extensions.add("${manga.source}:${it.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup categories
|
||||||
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
|
backupCategories(categoryEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup extension ID/name mapping
|
||||||
|
backupExtensionInfo(extensionEntries, extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When BackupCreatorJob
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file and create
|
||||||
|
var dir = UniFile.fromUri(context, uri)
|
||||||
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
newFile.openOutputStream().bufferedWriter().use {
|
||||||
|
parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFile.uri.toString()
|
||||||
|
} else {
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
file.openOutputStream().bufferedWriter().use {
|
||||||
|
parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.uri.toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||||
|
extensions.sorted().forEach {
|
||||||
|
root.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @param root root of categories json
|
||||||
|
*/
|
||||||
|
internal fun backupCategories(root: JsonArray) {
|
||||||
|
val categories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @return [JsonElement] containing manga information
|
||||||
|
*/
|
||||||
|
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
||||||
|
// Entry for this manga
|
||||||
|
val entry = JsonObject()
|
||||||
|
|
||||||
|
// Backup manga fields
|
||||||
|
entry[MANGA] = parser.toJsonTree(manga)
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
|
entry[CHAPTERS] = chaptersJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
if (historyForManga.isNotEmpty()) {
|
||||||
|
val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
url?.let { DHistory(url, history.last_read) }
|
||||||
|
}
|
||||||
|
val historyJson = parser.toJsonTree(historyData)
|
||||||
|
if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
entry[HISTORY] = historyJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
|
manga.id = dbManga.id
|
||||||
|
manga.copyFrom(dbManga)
|
||||||
|
manga.favorite = true
|
||||||
|
insertManga(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||||
|
return source.fetchMangaDetails(manga)
|
||||||
|
.map { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
manga.favorite = true
|
||||||
|
manga.initialized = true
|
||||||
|
manga.id = insertManga(manga)
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return (
|
||||||
|
if (source is EHentai) {
|
||||||
|
source.fetchChapterList(manga, throttleManager::throttle)
|
||||||
|
} else {
|
||||||
|
source.fetchChapterList(manga)
|
||||||
|
}
|
||||||
|
).map {
|
||||||
|
if (it.last().chapter_number == -99F) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
|
||||||
|
}
|
||||||
|
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
||||||
|
} else {
|
||||||
|
syncChaptersWithSource(databaseHelper, it, manga, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext { pair ->
|
||||||
|
if (pair.first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
insertChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the categories from Json
|
||||||
|
*
|
||||||
|
* @param jsonCategories array containing categories
|
||||||
|
*/
|
||||||
|
internal fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
|
// Get categories from file and from db
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupCategories.forEach { category ->
|
||||||
|
// Used to know if the category is already in the db
|
||||||
|
var found = false
|
||||||
|
for (dbCategory in dbCategories) {
|
||||||
|
// If the category is already in the db, assign the id to the file's category
|
||||||
|
// and do nothing
|
||||||
|
if (category.nameLower == dbCategory.nameLower) {
|
||||||
|
category.id = dbCategory.id
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the category isn't in the db, remove the id and insert a new category
|
||||||
|
// Store the inserted id in the category
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
category.id = null
|
||||||
|
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||||
|
category.id = result.insertedId()?.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the categories a manga is in.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose categories have to be restored.
|
||||||
|
* @param categories the categories to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||||
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||||
|
for (backupCategoryStr in categories) {
|
||||||
|
for (dbCategory in dbCategories) {
|
||||||
|
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
||||||
|
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
|
val mangaAsList = ArrayList<Manga>()
|
||||||
|
mangaAsList.add(manga)
|
||||||
|
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||||
|
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val historyToBeUpdated = ArrayList<History>()
|
||||||
|
for ((url, lastRead) in history) {
|
||||||
|
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory.apply {
|
||||||
|
last_read = max(lastRead, dbHistory.last_read)
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(dbHistory)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||||
|
val historyToAdd = History.create(it).apply {
|
||||||
|
last_read = lastRead
|
||||||
|
}
|
||||||
|
historyToBeUpdated.add(historyToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the sync of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose sync have to be restored.
|
||||||
|
* @param tracks the track list to restore.
|
||||||
|
*/
|
||||||
|
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
tracks.map { it.manga_id = manga.id!! }
|
||||||
|
|
||||||
|
// Get tracks from database
|
||||||
|
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
val trackToUpdate = ArrayList<Track>()
|
||||||
|
|
||||||
|
for (track in tracks) {
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
var isInDatabase = false
|
||||||
|
for (dbTrack in dbTracks) {
|
||||||
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
|
// The sync is already in the db, only update its fields
|
||||||
|
if (track.media_id != dbTrack.media_id) {
|
||||||
|
dbTrack.media_id = track.media_id
|
||||||
|
}
|
||||||
|
if (track.library_id != dbTrack.library_id) {
|
||||||
|
dbTrack.library_id = track.library_id
|
||||||
|
}
|
||||||
|
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
track.id = null
|
||||||
|
trackToUpdate.add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update database
|
||||||
|
if (trackToUpdate.isNotEmpty()) {
|
||||||
|
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the chapters for manga if chapters already in database
|
||||||
|
*
|
||||||
|
* @param manga manga of chapters
|
||||||
|
* @param chapters list containing chapters that get restored
|
||||||
|
* @return boolean answering if chapter fetch is not needed
|
||||||
|
*/
|
||||||
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// Return if fetch is needed
|
||||||
|
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (chapter in chapters) {
|
||||||
|
val pos = dbChapters.indexOf(chapter)
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
chapter.id = dbChapter.id
|
||||||
|
chapter.copyFrom(dbChapter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
chapters.filter { it.id != null }
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
insertChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||||
|
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list containing manga from library
|
||||||
|
*
|
||||||
|
* @return [Manga] from library
|
||||||
|
*/
|
||||||
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
internal fun insertManga(manga: Manga): Long? =
|
||||||
|
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
private fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||||
|
}
|
@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BackupNotifier(private val context: Context) {
|
internal class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
@ -24,7 +24,6 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
@ -42,6 +41,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.creating_backup))
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
@ -65,10 +65,15 @@ class BackupNotifier(private val context: Context) {
|
|||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.backup_created))
|
setContentTitle(context.getString(R.string.backup_created))
|
||||||
setContentText(unifile.filePath ?: unifile.name)
|
|
||||||
|
if (unifile.filePath != null) {
|
||||||
|
setContentText(unifile.filePath)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
clearActions()
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
@ -92,7 +97,9 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
clearActions()
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_close_24dp,
|
R.drawable.ic_close_24dp,
|
||||||
@ -133,15 +140,17 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||||
|
|
||||||
// Clear old actions if they exist
|
// Clear old actions if they exist
|
||||||
clearActions()
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.nnf_ic_file_folder,
|
||||||
context.getString(R.string.action_show_errors),
|
context.getString(R.string.action_open_log),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,25 +4,52 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.core.content.ContextCompat
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import exh.EXHMigrations
|
||||||
|
import exh.eh.EHentaiThrottleManager
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup.
|
* Restores backup from a JSON file.
|
||||||
*/
|
*/
|
||||||
class BackupRestoreService : Service() {
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
@ -43,13 +70,16 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, mode: Int) {
|
fun start(context: Context, uri: Uri) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,14 +100,44 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
private lateinit var ioScope: CoroutineScope
|
private var job: Job? = null
|
||||||
private var backupRestore: AbstractBackupRestore<*>? = null
|
|
||||||
|
private val throttleManager = EHentaiThrottleManager()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress of a backup restore
|
||||||
|
*/
|
||||||
|
private var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of manga in Json file (needed for restore)
|
||||||
|
*/
|
||||||
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
private var skippedAmount = 0
|
||||||
|
|
||||||
|
private var totalAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of source ID to source name from backup data
|
||||||
|
*/
|
||||||
|
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing errors
|
||||||
|
*/
|
||||||
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
private lateinit var notifier: BackupNotifier
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
notifier = BackupNotifier(this)
|
notifier = BackupNotifier(this)
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
@ -95,8 +155,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
job?.cancel()
|
||||||
ioScope.cancel()
|
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -117,33 +176,323 @@ class BackupRestoreService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
|
||||||
|
throttleManager.resetThrottle()
|
||||||
|
|
||||||
// Cancel any previous job if needed.
|
// Cancel any previous job if needed.
|
||||||
backupRestore?.job?.cancel()
|
job?.cancel()
|
||||||
|
|
||||||
backupRestore = when (mode) {
|
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
|
||||||
else -> LegacyBackupRestore(this, notifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
backupRestore?.writeErrorLog()
|
writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
val job = ioScope.launch(handler) {
|
job = GlobalScope.launch(handler) {
|
||||||
if (backupRestore?.restoreBackup(uri) == false) {
|
if (!restoreBackup(uri)) {
|
||||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
job.invokeOnCompletion {
|
job?.invokeOnCompletion {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
backupRestore?.job = job
|
|
||||||
|
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores data from backup file.
|
||||||
|
*
|
||||||
|
* @param uri backup file to restore
|
||||||
|
*/
|
||||||
|
private fun restoreBackup(uri: Uri): Boolean {
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
|
val validManga = mangasJson.filter {
|
||||||
|
var manga = backupManager.parser.fromJson<MangaImpl>(it.asJsonObject.get(MANGA))
|
||||||
|
// EXH -->
|
||||||
|
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||||
|
val sourced = backupManager.sourceManager.get(manga.source) != null
|
||||||
|
if (!sourced) {
|
||||||
|
restoreAmount -= 1
|
||||||
|
}
|
||||||
|
sourced
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAmount = mangasJson.size()
|
||||||
|
restoreAmount = validManga.count() + 1 // +1 for categories
|
||||||
|
skippedAmount = mangasJson.size() - validManga.count()
|
||||||
|
restoreProgress = 0
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
restoreCategories(json.get(CATEGORIES))
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
mangasJson.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it.asJsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
|
||||||
|
val logFile = writeErrorLog()
|
||||||
|
|
||||||
|
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
var manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
// EXH -->
|
||||||
|
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||||
|
// <-- EXH
|
||||||
|
|
||||||
|
try {
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
val message = if (manga.source in sourceMapping) {
|
||||||
|
getString(R.string.source_not_found_name, sourceMapping[manga.source])
|
||||||
|
} else {
|
||||||
|
getString(R.string.source_not_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.add(Date() to "${manga.title} - $message")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
*/
|
||||||
|
private fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun restoreMangaFetch(
|
||||||
|
source: Source,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap {
|
||||||
|
chapterFetchObservable(source, it, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
trackingFetchObservable(it, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreMangaNoFetch(
|
||||||
|
source: Source,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks)
|
||||||
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
|
trackingFetchObservable(manga, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
Pair(emptyList(), emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that refreshes tracking information
|
||||||
|
* @param manga manga that needs updating.
|
||||||
|
* @param tracks list containing tracks from restore file.
|
||||||
|
* @return [Observable] that contains updated track item
|
||||||
|
*/
|
||||||
|
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||||
|
return Observable.from(tracks)
|
||||||
|
.flatMap { track ->
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service.isLogged) {
|
||||||
|
service.refresh(track)
|
||||||
|
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
track
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||||
|
Observable.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [BackupConst]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(
|
||||||
|
progress: Int,
|
||||||
|
amount: Int,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
private fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
|
||||||
|
object BackupRestoreValidator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for critical backup file data.
|
||||||
|
*
|
||||||
|
* @throws Exception if version or manga cannot be found.
|
||||||
|
* @return List of required sources.
|
||||||
|
*/
|
||||||
|
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
val version = json.get(Backup.VERSION)
|
||||||
|
val mangasJson = json.get(Backup.MANGAS)
|
||||||
|
if (version == null || mangasJson == null) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mangasJson.asJsonArray.size() == 0) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSourceMapping(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
|
return extensionsMapping.asJsonArray
|
||||||
|
.map {
|
||||||
|
val items = it.asString.split(":")
|
||||||
|
items[0].toLong() to items[1]
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
}
|
@ -1,352 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import okio.sink
|
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|
||||||
|
|
||||||
val parser = ProtoBuf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create backup Json file from database
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isJob backup called from job
|
|
||||||
*/
|
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
|
||||||
// Create root object
|
|
||||||
var backup: Backup? = null
|
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
|
||||||
val databaseManga = getFavoriteManga()
|
|
||||||
|
|
||||||
backup = Backup(
|
|
||||||
backupManga(databaseManga, flags),
|
|
||||||
backupCategories(),
|
|
||||||
backupExtensionInfo(databaseManga)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val file: UniFile = (
|
|
||||||
if (isJob) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = numberOfBackups()
|
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
|
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
dir.createFile(BackupFull.getDefaultFilename())
|
|
||||||
} else {
|
|
||||||
UniFile.fromUri(context, uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
|
||||||
return file.uri.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
|
||||||
return mangas.map {
|
|
||||||
backupMangaObject(it, flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
|
|
||||||
return mangas
|
|
||||||
.asSequence()
|
|
||||||
.map { it.source }
|
|
||||||
.distinct()
|
|
||||||
.map { sourceManager.getOrStub(it) }
|
|
||||||
.map { BackupSource.copyFrom(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @return list of [BackupCategory] to be backed up
|
|
||||||
*/
|
|
||||||
private fun backupCategories(): List<BackupCategory> {
|
|
||||||
return databaseHelper.getCategories()
|
|
||||||
.executeAsBlocking()
|
|
||||||
.map { BackupCategory.copyFrom(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @param options options for the backup
|
|
||||||
* @return [BackupManga] containing manga in a serializable form
|
|
||||||
*/
|
|
||||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
|
||||||
// Entry for this manga
|
|
||||||
val mangaObject = BackupManga.copyFrom(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
|
||||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
if (historyForManga.isNotEmpty()) {
|
|
||||||
val history = historyForManga.mapNotNull { history ->
|
|
||||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
|
||||||
url?.let { BackupHistory(url, history.last_read) }
|
|
||||||
}
|
|
||||||
if (history.isNotEmpty()) {
|
|
||||||
mangaObject.history = history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaObject
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
|
||||||
manga.id = dbManga.id
|
|
||||||
manga.copyFrom(dbManga)
|
|
||||||
insertManga(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga info.
|
|
||||||
*/
|
|
||||||
fun restoreManga(manga: Manga): Manga {
|
|
||||||
return manga.also {
|
|
||||||
it.initialized = it.description != null
|
|
||||||
it.id = insertManga(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the categories from Json
|
|
||||||
*
|
|
||||||
* @param backupCategories list containing categories
|
|
||||||
*/
|
|
||||||
internal fun restoreCategories(backupCategories: List<BackupCategory>) {
|
|
||||||
// Get categories from file and from db
|
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
|
|
||||||
// Iterate over them
|
|
||||||
backupCategories.map { it.getCategoryImpl() }.forEach { category ->
|
|
||||||
// Used to know if the category is already in the db
|
|
||||||
var found = false
|
|
||||||
for (dbCategory in dbCategories) {
|
|
||||||
// If the category is already in the db, assign the id to the file's category
|
|
||||||
// and do nothing
|
|
||||||
if (category.name == dbCategory.name) {
|
|
||||||
category.id = dbCategory.id
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the category isn't in the db, remove the id and insert a new category
|
|
||||||
// Store the inserted id in the category
|
|
||||||
if (!found) {
|
|
||||||
// Let the db assign the id
|
|
||||||
category.id = null
|
|
||||||
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
|
||||||
category.id = result.insertedId()?.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories a manga is in.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose categories have to be restored.
|
|
||||||
* @param categories the categories to restore.
|
|
||||||
*/
|
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
|
||||||
categories.forEach { backupCategoryOrder ->
|
|
||||||
backupCategories.firstOrNull {
|
|
||||||
it.order == backupCategoryOrder
|
|
||||||
}?.let { backupCategory ->
|
|
||||||
dbCategories.firstOrNull { dbCategory ->
|
|
||||||
dbCategory.name == backupCategory.name
|
|
||||||
}?.let { dbCategory ->
|
|
||||||
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
|
||||||
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
|
||||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore history from Json
|
|
||||||
*
|
|
||||||
* @param history list containing history to be restored
|
|
||||||
*/
|
|
||||||
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
|
||||||
// List containing history to be updated
|
|
||||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
|
||||||
for ((url, lastRead) in history) {
|
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
|
||||||
// Check if history already in database and update
|
|
||||||
if (dbHistory != null) {
|
|
||||||
dbHistory.apply {
|
|
||||||
last_read = max(lastRead, dbHistory.last_read)
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(dbHistory)
|
|
||||||
} else {
|
|
||||||
// If not in database create
|
|
||||||
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
|
||||||
val historyToAdd = History.create(it).apply {
|
|
||||||
last_read = lastRead
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(historyToAdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the sync of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose sync have to be restored.
|
|
||||||
* @param tracks the track list to restore.
|
|
||||||
*/
|
|
||||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
tracks.map { it.manga_id = manga.id!! }
|
|
||||||
|
|
||||||
// Get tracks from database
|
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
val trackToUpdate = mutableListOf<Track>()
|
|
||||||
|
|
||||||
tracks.forEach { track ->
|
|
||||||
var isInDatabase = false
|
|
||||||
for (dbTrack in dbTracks) {
|
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
|
||||||
// The sync is already in the db, only update its fields
|
|
||||||
if (track.media_id != dbTrack.media_id) {
|
|
||||||
dbTrack.media_id = track.media_id
|
|
||||||
}
|
|
||||||
if (track.library_id != dbTrack.library_id) {
|
|
||||||
dbTrack.library_id = track.library_id
|
|
||||||
}
|
|
||||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
track.id = null
|
|
||||||
trackToUpdate.add(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update database
|
|
||||||
if (trackToUpdate.isNotEmpty()) {
|
|
||||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
|
||||||
if (dbChapter != null) {
|
|
||||||
chapter.id = dbChapter.id
|
|
||||||
chapter.copyFrom(dbChapter)
|
|
||||||
if (dbChapter.read && !chapter.read) {
|
|
||||||
chapter.read = dbChapter.read
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
}
|
|
||||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
|
||||||
chapter.bookmark = dbChapter.bookmark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val newChapters = chapters.groupBy { it.id != null }
|
|
||||||
newChapters[true]?.let { updateKnownChapters(it) }
|
|
||||||
newChapters[false]?.let { insertChapters(it) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import okio.source
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
|
||||||
backupManager = FullBackupManager(context)
|
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
|
||||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
|
||||||
|
|
||||||
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
if (backup.backupCategories.isNotEmpty()) {
|
|
||||||
restoreCategories(backup.backupCategories)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
|
||||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
backup.backupManga.forEach {
|
|
||||||
if (job?.isActive != true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreManga(it, backup.backupCategories)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(backupCategories: List<BackupCategory>) {
|
|
||||||
db.inTransaction {
|
|
||||||
backupManager.restoreCategories(backupCategories)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
|
||||||
val manga = backupManga.getMangaImpl()
|
|
||||||
val chapters = backupManga.getChaptersImpl()
|
|
||||||
val categories = backupManga.categories
|
|
||||||
val history = backupManga.history
|
|
||||||
val tracks = backupManga.getTrackingImpl()
|
|
||||||
|
|
||||||
try {
|
|
||||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a manga restore observable
|
|
||||||
*
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
*/
|
|
||||||
private fun restoreMangaData(
|
|
||||||
manga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<Int>,
|
|
||||||
history: List<BackupHistory>,
|
|
||||||
tracks: List<Track>,
|
|
||||||
backupCategories: List<BackupCategory>
|
|
||||||
) {
|
|
||||||
db.inTransaction {
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Manga not in database
|
|
||||||
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
|
|
||||||
} else {
|
|
||||||
// Manga in database
|
|
||||||
// Copy information from manga already in database
|
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
// Fetch rest of manga information
|
|
||||||
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
|
||||||
private fun restoreMangaFetch(
|
|
||||||
manga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<Int>,
|
|
||||||
history: List<BackupHistory>,
|
|
||||||
tracks: List<Track>,
|
|
||||||
backupCategories: List<BackupCategory>
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val fetchedManga = backupManager.restoreManga(manga)
|
|
||||||
fetchedManga.id ?: return
|
|
||||||
|
|
||||||
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreMangaNoFetch(
|
|
||||||
backupManga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<Int>,
|
|
||||||
history: List<BackupHistory>,
|
|
||||||
tracks: List<Track>,
|
|
||||||
backupCategories: List<BackupCategory>
|
|
||||||
) {
|
|
||||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import okio.source
|
|
||||||
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|
||||||
/**
|
|
||||||
* Checks for critical backup file data.
|
|
||||||
*
|
|
||||||
* @throws Exception if manga cannot be found.
|
|
||||||
* @return List of missing sources or missing trackers.
|
|
||||||
*/
|
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
|
||||||
val backupManager = FullBackupManager(context)
|
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
|
||||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
|
||||||
|
|
||||||
if (backup.backupManga.isEmpty()) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
|
||||||
val missingSources = sources
|
|
||||||
.filter { sourceManager.get(it.key) == null }
|
|
||||||
.values
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
val trackers = backup.backupManga
|
|
||||||
.flatMap { it.tracking }
|
|
||||||
.map { it.syncId }
|
|
||||||
.distinct()
|
|
||||||
val missingTrackers = trackers
|
|
||||||
.mapNotNull { trackManager.getService(it) }
|
|
||||||
.filter { !it.isLogged }
|
|
||||||
.map { context.getString(it.nameRes()) }
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
return Results(missingSources, missingTrackers)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Backup(
|
|
||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
|
||||||
)
|
|
@ -1,33 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class BackupCategory(
|
|
||||||
@ProtoNumber(1) var name: String,
|
|
||||||
@ProtoNumber(2) var order: Int = 0,
|
|
||||||
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
|
||||||
// Bump by 100 to specify this is a 0.x value
|
|
||||||
@ProtoNumber(100) var flags: Int = 0,
|
|
||||||
) {
|
|
||||||
fun getCategoryImpl(): CategoryImpl {
|
|
||||||
return CategoryImpl().apply {
|
|
||||||
name = this@BackupCategory.name
|
|
||||||
flags = this@BackupCategory.flags
|
|
||||||
order = this@BackupCategory.order
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(category: Category): BackupCategory {
|
|
||||||
return BackupCategory(
|
|
||||||
name = category.name,
|
|
||||||
order = category.order,
|
|
||||||
flags = category.flags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupChapter(
|
|
||||||
// in 1.x some of these values have different names
|
|
||||||
// url is called key in 1.x
|
|
||||||
@ProtoNumber(1) var url: String,
|
|
||||||
@ProtoNumber(2) var name: String,
|
|
||||||
@ProtoNumber(3) var scanlator: String? = null,
|
|
||||||
@ProtoNumber(4) var read: Boolean = false,
|
|
||||||
@ProtoNumber(5) var bookmark: Boolean = false,
|
|
||||||
// lastPageRead is called progress in 1.x
|
|
||||||
@ProtoNumber(6) var lastPageRead: Int = 0,
|
|
||||||
@ProtoNumber(7) var dateFetch: Long = 0,
|
|
||||||
@ProtoNumber(8) var dateUpload: Long = 0,
|
|
||||||
// chapterNumber is called number is 1.x
|
|
||||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
|
||||||
@ProtoNumber(10) var sourceOrder: Int = 0,
|
|
||||||
) {
|
|
||||||
fun toChapterImpl(): ChapterImpl {
|
|
||||||
return ChapterImpl().apply {
|
|
||||||
url = this@BackupChapter.url
|
|
||||||
name = this@BackupChapter.name
|
|
||||||
chapter_number = this@BackupChapter.chapterNumber
|
|
||||||
scanlator = this@BackupChapter.scanlator
|
|
||||||
read = this@BackupChapter.read
|
|
||||||
bookmark = this@BackupChapter.bookmark
|
|
||||||
last_page_read = this@BackupChapter.lastPageRead
|
|
||||||
date_fetch = this@BackupChapter.dateFetch
|
|
||||||
date_upload = this@BackupChapter.dateUpload
|
|
||||||
source_order = this@BackupChapter.sourceOrder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(chapter: Chapter): BackupChapter {
|
|
||||||
return BackupChapter(
|
|
||||||
url = chapter.url,
|
|
||||||
name = chapter.name,
|
|
||||||
chapterNumber = chapter.chapter_number,
|
|
||||||
scanlator = chapter.scanlator,
|
|
||||||
read = chapter.read,
|
|
||||||
bookmark = chapter.bookmark,
|
|
||||||
lastPageRead = chapter.last_page_read,
|
|
||||||
dateFetch = chapter.date_fetch,
|
|
||||||
dateUpload = chapter.date_upload,
|
|
||||||
sourceOrder = chapter.source_order
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object BackupFull {
|
|
||||||
fun getDefaultFilename(): String {
|
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
|
||||||
return "tachiyomi_$date.proto.gz"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupHistory(
|
|
||||||
@ProtoNumber(0) var url: String,
|
|
||||||
@ProtoNumber(1) var lastRead: Long
|
|
||||||
)
|
|
@ -1,89 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupManga(
|
|
||||||
// in 1.x some of these values have different names
|
|
||||||
@ProtoNumber(1) var source: Long,
|
|
||||||
// url is called key in 1.x
|
|
||||||
@ProtoNumber(2) var url: String,
|
|
||||||
@ProtoNumber(3) var title: String = "",
|
|
||||||
@ProtoNumber(4) var artist: String? = null,
|
|
||||||
@ProtoNumber(5) var author: String? = null,
|
|
||||||
@ProtoNumber(6) var description: String? = null,
|
|
||||||
@ProtoNumber(7) var genre: List<String> = emptyList(),
|
|
||||||
@ProtoNumber(8) var status: Int = 0,
|
|
||||||
// thumbnailUrl is called cover in 1.x
|
|
||||||
@ProtoNumber(9) var thumbnailUrl: String? = null,
|
|
||||||
// @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x
|
|
||||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
|
||||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
|
||||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
|
||||||
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
|
||||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
|
||||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
|
||||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
|
||||||
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
|
|
||||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
|
||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
|
||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
|
||||||
@ProtoNumber(103) var viewer_flags: Int? = null
|
|
||||||
) {
|
|
||||||
fun getMangaImpl(): MangaImpl {
|
|
||||||
return MangaImpl().apply {
|
|
||||||
url = this@BackupManga.url
|
|
||||||
title = this@BackupManga.title
|
|
||||||
artist = this@BackupManga.artist
|
|
||||||
author = this@BackupManga.author
|
|
||||||
description = this@BackupManga.description
|
|
||||||
genre = this@BackupManga.genre.joinToString()
|
|
||||||
status = this@BackupManga.status
|
|
||||||
thumbnail_url = this@BackupManga.thumbnailUrl
|
|
||||||
favorite = this@BackupManga.favorite
|
|
||||||
source = this@BackupManga.source
|
|
||||||
date_added = this@BackupManga.dateAdded
|
|
||||||
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
|
||||||
chapter_flags = this@BackupManga.chapterFlags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChaptersImpl(): List<ChapterImpl> {
|
|
||||||
return chapters.map {
|
|
||||||
it.toChapterImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTrackingImpl(): List<TrackImpl> {
|
|
||||||
return tracking.map {
|
|
||||||
it.getTrackingImpl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(manga: Manga): BackupManga {
|
|
||||||
return BackupManga(
|
|
||||||
url = manga.url,
|
|
||||||
title = manga.title,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.getGenres() ?: emptyList(),
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnail_url,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
source = manga.source,
|
|
||||||
dateAdded = manga.date_added,
|
|
||||||
viewer = manga.readingModeType,
|
|
||||||
viewer_flags = manga.viewer_flags,
|
|
||||||
chapterFlags = manga.chapter_flags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
|
|
||||||
@Serializer(forClass = Backup::class)
|
|
||||||
object BackupSerializer
|
|
@ -1,20 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupSource(
|
|
||||||
@ProtoNumber(0) var name: String = "",
|
|
||||||
@ProtoNumber(1) var sourceId: Long
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(source: Source): BackupSource {
|
|
||||||
return BackupSource(
|
|
||||||
name = source.name,
|
|
||||||
sourceId = source.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.full.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupTracking(
|
|
||||||
// in 1.x some of these values have different types or names
|
|
||||||
// syncId is called siteId in 1,x
|
|
||||||
@ProtoNumber(1) var syncId: Int,
|
|
||||||
// LibraryId is not null in 1.x
|
|
||||||
@ProtoNumber(2) var libraryId: Long,
|
|
||||||
@ProtoNumber(3) var mediaId: Int = 0,
|
|
||||||
// trackingUrl is called mediaUrl in 1.x
|
|
||||||
@ProtoNumber(4) var trackingUrl: String = "",
|
|
||||||
@ProtoNumber(5) var title: String = "",
|
|
||||||
// lastChapterRead is called last read, and it has been changed to a float in 1.x
|
|
||||||
@ProtoNumber(6) var lastChapterRead: Float = 0F,
|
|
||||||
@ProtoNumber(7) var totalChapters: Int = 0,
|
|
||||||
@ProtoNumber(8) var score: Float = 0F,
|
|
||||||
@ProtoNumber(9) var status: Int = 0,
|
|
||||||
// startedReadingDate is called startReadTime in 1.x
|
|
||||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
|
||||||
// finishedReadingDate is called endReadTime in 1.x
|
|
||||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
|
||||||
) {
|
|
||||||
fun getTrackingImpl(): TrackImpl {
|
|
||||||
return TrackImpl().apply {
|
|
||||||
sync_id = this@BackupTracking.syncId
|
|
||||||
media_id = this@BackupTracking.mediaId
|
|
||||||
library_id = this@BackupTracking.libraryId
|
|
||||||
title = this@BackupTracking.title
|
|
||||||
// convert from float to int because of 1.x types
|
|
||||||
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
|
||||||
total_chapters = this@BackupTracking.totalChapters
|
|
||||||
score = this@BackupTracking.score
|
|
||||||
status = this@BackupTracking.status
|
|
||||||
started_reading_date = this@BackupTracking.startedReadingDate
|
|
||||||
finished_reading_date = this@BackupTracking.finishedReadingDate
|
|
||||||
tracking_url = this@BackupTracking.trackingUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun copyFrom(track: Track): BackupTracking {
|
|
||||||
return BackupTracking(
|
|
||||||
syncId = track.sync_id,
|
|
||||||
mediaId = track.media_id,
|
|
||||||
// forced not null so its compatible with 1.x backup system
|
|
||||||
libraryId = track.library_id!!,
|
|
||||||
title = track.title,
|
|
||||||
// convert to float for 1.x
|
|
||||||
lastChapterRead = track.last_chapter_read.toFloat(),
|
|
||||||
totalChapters = track.total_chapters,
|
|
||||||
score = track.score,
|
|
||||||
status = track.status,
|
|
||||||
startedReadingDate = track.started_reading_date,
|
|
||||||
finishedReadingDate = track.finished_reading_date,
|
|
||||||
trackingUrl = track.tracking_url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,252 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.modules.SerializersModule
|
|
||||||
import kotlinx.serialization.modules.contextual
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
|
||||||
|
|
||||||
val parser: Json = when (version) {
|
|
||||||
2 -> Json {
|
|
||||||
// Forks may have added items to backup
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
|
|
||||||
// Register custom serializers
|
|
||||||
serializersModule = SerializersModule {
|
|
||||||
contextual(MangaTypeSerializer)
|
|
||||||
contextual(MangaImplTypeSerializer)
|
|
||||||
contextual(ChapterTypeSerializer)
|
|
||||||
contextual(ChapterImplTypeSerializer)
|
|
||||||
contextual(CategoryTypeSerializer)
|
|
||||||
contextual(CategoryImplTypeSerializer)
|
|
||||||
contextual(TrackTypeSerializer)
|
|
||||||
contextual(TrackImplTypeSerializer)
|
|
||||||
contextual(HistoryTypeSerializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw Exception("Unknown backup version")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create backup Json file from database
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isJob backup called from job
|
|
||||||
*/
|
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
|
||||||
throw IllegalStateException("Legacy backup creation is not supported")
|
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
|
||||||
manga.id = dbManga.id
|
|
||||||
manga.copyFrom(dbManga)
|
|
||||||
manga.favorite = true
|
|
||||||
insertManga(manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga.
|
|
||||||
*/
|
|
||||||
suspend fun fetchManga(source: Source, manga: Manga): Manga {
|
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
|
||||||
return manga.also {
|
|
||||||
it.copyFrom(networkManga.toSManga())
|
|
||||||
it.favorite = true
|
|
||||||
it.initialized = true
|
|
||||||
it.id = insertManga(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the categories from Json
|
|
||||||
*
|
|
||||||
* @param backupCategories array containing categories
|
|
||||||
*/
|
|
||||||
internal fun restoreCategories(backupCategories: List<Category>) {
|
|
||||||
// Get categories from file and from db
|
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
|
|
||||||
// Iterate over them
|
|
||||||
backupCategories.forEach { category ->
|
|
||||||
// Used to know if the category is already in the db
|
|
||||||
var found = false
|
|
||||||
for (dbCategory in dbCategories) {
|
|
||||||
// If the category is already in the db, assign the id to the file's category
|
|
||||||
// and do nothing
|
|
||||||
if (category.name == dbCategory.name) {
|
|
||||||
category.id = dbCategory.id
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the category isn't in the db, remove the id and insert a new category
|
|
||||||
// Store the inserted id in the category
|
|
||||||
if (!found) {
|
|
||||||
// Let the db assign the id
|
|
||||||
category.id = null
|
|
||||||
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
|
||||||
category.id = result.insertedId()?.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories a manga is in.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose categories have to be restored.
|
|
||||||
* @param categories the categories to restore.
|
|
||||||
*/
|
|
||||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
|
||||||
for (backupCategoryStr in categories) {
|
|
||||||
for (dbCategory in dbCategories) {
|
|
||||||
if (backupCategoryStr == dbCategory.name) {
|
|
||||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
|
||||||
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
|
||||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore history from Json
|
|
||||||
*
|
|
||||||
* @param history list containing history to be restored
|
|
||||||
*/
|
|
||||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
|
||||||
// List containing history to be updated
|
|
||||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
|
||||||
for ((url, lastRead) in history) {
|
|
||||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
|
||||||
// Check if history already in database and update
|
|
||||||
if (dbHistory != null) {
|
|
||||||
dbHistory.apply {
|
|
||||||
last_read = max(lastRead, dbHistory.last_read)
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(dbHistory)
|
|
||||||
} else {
|
|
||||||
// If not in database create
|
|
||||||
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
|
||||||
val historyToAdd = History.create(it).apply {
|
|
||||||
last_read = lastRead
|
|
||||||
}
|
|
||||||
historyToBeUpdated.add(historyToAdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the sync of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose sync have to be restored.
|
|
||||||
* @param tracks the track list to restore.
|
|
||||||
*/
|
|
||||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
|
||||||
// Get tracks from database
|
|
||||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
val trackToUpdate = ArrayList<Track>(tracks.size)
|
|
||||||
|
|
||||||
tracks.forEach { track ->
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
track.manga_id = manga.id!!
|
|
||||||
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
var isInDatabase = false
|
|
||||||
for (dbTrack in dbTracks) {
|
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
|
||||||
// The sync is already in the db, only update its fields
|
|
||||||
if (track.media_id != dbTrack.media_id) {
|
|
||||||
dbTrack.media_id = track.media_id
|
|
||||||
}
|
|
||||||
if (track.library_id != dbTrack.library_id) {
|
|
||||||
dbTrack.library_id = track.library_id
|
|
||||||
}
|
|
||||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
track.id = null
|
|
||||||
trackToUpdate.add(track)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update database
|
|
||||||
if (trackToUpdate.isNotEmpty()) {
|
|
||||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the chapters for manga if chapters already in database
|
|
||||||
*
|
|
||||||
* @param manga manga of chapters
|
|
||||||
* @param chapters list containing chapters that get restored
|
|
||||||
* @return boolean answering if chapter fetch is not needed
|
|
||||||
*/
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Return if fetch is needed
|
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (chapter in chapters) {
|
|
||||||
val pos = dbChapters.indexOf(chapter)
|
|
||||||
if (pos != -1) {
|
|
||||||
val dbChapter = dbChapters[pos]
|
|
||||||
chapter.id = dbChapter.id
|
|
||||||
chapter.copyFrom(dbChapter)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
updateChapters(chapters.filter { it.id != null })
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
|
||||||
// Read the json and create a Json Object,
|
|
||||||
// cannot use the backupManager json deserializer one because its not initialized yet
|
|
||||||
val backupObject = Json.decodeFromString<JsonObject>(
|
|
||||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get parser version
|
|
||||||
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
|
||||||
|
|
||||||
// Initialize manager
|
|
||||||
backupManager = LegacyBackupManager(context, version)
|
|
||||||
|
|
||||||
// Decode the json object to a Backup object
|
|
||||||
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
|
||||||
|
|
||||||
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
backup.categories?.let { restoreCategories(it) }
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
backup.mangas.forEach {
|
|
||||||
if (job?.isActive != true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreManga(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: List<Category>) {
|
|
||||||
db.inTransaction {
|
|
||||||
backupManager.restoreCategories(categoriesJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: MangaObject) {
|
|
||||||
val manga = mangaJson.manga
|
|
||||||
val chapters = mangaJson.chapters ?: emptyList()
|
|
||||||
val categories = mangaJson.categories ?: emptyList()
|
|
||||||
val history = mangaJson.history ?: emptyList()
|
|
||||||
val tracks = mangaJson.track ?: emptyList()
|
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (source != null) {
|
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a manga restore observable
|
|
||||||
*
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param source source to get manga data from
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
*/
|
|
||||||
private suspend fun restoreMangaData(
|
|
||||||
manga: Manga,
|
|
||||||
source: Source,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
db.inTransaction {
|
|
||||||
if (dbManga == null) {
|
|
||||||
// Manga not in database
|
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
|
||||||
} else { // Manga in database
|
|
||||||
// Copy information from manga already in database
|
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
|
||||||
// Fetch rest of manga information
|
|
||||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information.
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @param chapters chapters of manga that needs updating
|
|
||||||
* @param categories categories that need updating
|
|
||||||
*/
|
|
||||||
private suspend fun restoreMangaFetch(
|
|
||||||
source: Source,
|
|
||||||
manga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
|
||||||
fetchedManga.id ?: return
|
|
||||||
|
|
||||||
updateChapters(source, fetchedManga, chapters)
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
|
||||||
|
|
||||||
updateTracking(fetchedManga, tracks)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun restoreMangaNoFetch(
|
|
||||||
source: Source,
|
|
||||||
backupManga: Manga,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
|
||||||
updateChapters(source, backupManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks)
|
|
||||||
|
|
||||||
updateTracking(backupManga, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
|
||||||
// Restore categories
|
|
||||||
backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// Restore history
|
|
||||||
backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// Restore tracking
|
|
||||||
backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|
||||||
/**
|
|
||||||
* Checks for critical backup file data.
|
|
||||||
*
|
|
||||||
* @throws Exception if version or manga cannot be found.
|
|
||||||
* @return List of missing sources or missing trackers.
|
|
||||||
*/
|
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
|
||||||
val backupManager = LegacyBackupManager(context)
|
|
||||||
|
|
||||||
val backup = backupManager.parser.decodeFromString<Backup>(
|
|
||||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (backup.version == null) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backup.mangas.isEmpty()) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
|
||||||
val missingSources = sources
|
|
||||||
.filter { sourceManager.get(it.key) == null }
|
|
||||||
.values
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
val trackers = backup.mangas
|
|
||||||
.filterNot { it.track.isNullOrEmpty() }
|
|
||||||
.flatMap { it.track ?: emptyList() }
|
|
||||||
.map { it.sync_id }
|
|
||||||
.distinct()
|
|
||||||
val missingTrackers = trackers
|
|
||||||
.mapNotNull { trackManager.getService(it) }
|
|
||||||
.filter { !it.isLogged }
|
|
||||||
.map { context.getString(it.nameRes()) }
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
return Results(missingSources, missingTrackers)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
|
||||||
return extensionsMapping
|
|
||||||
.map {
|
|
||||||
val items = it.split(":")
|
|
||||||
items[0].toLong() to items[1]
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import kotlinx.serialization.Contextual
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Backup(
|
|
||||||
val version: Int? = null,
|
|
||||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
|
||||||
var categories: List<@Contextual Category>? = null,
|
|
||||||
var extensions: List<String>? = null
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val CURRENT_VERSION = 2
|
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
|
||||||
return "tachiyomi_$date.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MangaObject(
|
|
||||||
var manga: @Contextual Manga,
|
|
||||||
var chapters: List<@Contextual Chapter>? = null,
|
|
||||||
var categories: List<String>? = null,
|
|
||||||
var track: List<@Contextual Track>? = null,
|
|
||||||
var history: List<@Contextual DHistory>? = null
|
|
||||||
)
|
|
@ -1,49 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.name)
|
|
||||||
add(value.order)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a category impl and cast as T so that the serializer accepts it
|
|
||||||
return CategoryImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
name = array[0].jsonPrimitive.content
|
|
||||||
order = array[1].jsonPrimitive.int
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a category and category impl
|
|
||||||
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
|
||||||
|
|
||||||
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
|
@ -1,66 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
|
||||||
|
|
||||||
override val descriptor = buildClassSerialDescriptor("Chapter")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonObject {
|
|
||||||
put(URL, value.url)
|
|
||||||
if (value.read) {
|
|
||||||
put(READ, 1)
|
|
||||||
}
|
|
||||||
if (value.bookmark) {
|
|
||||||
put(BOOKMARK, 1)
|
|
||||||
}
|
|
||||||
if (value.last_page_read != 0) {
|
|
||||||
put(LAST_READ, value.last_page_read)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a chapter impl and cast as T so that the serializer accepts it
|
|
||||||
return ChapterImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val jsonObject = decoder.decodeJsonElement().jsonObject
|
|
||||||
url = jsonObject[URL]!!.jsonPrimitive.content
|
|
||||||
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
|
||||||
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
|
||||||
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a chapter and chapter impl
|
|
||||||
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
|
||||||
|
|
||||||
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeSerializer : KSerializer<DHistory> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: DHistory) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.url)
|
|
||||||
add(value.lastRead)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): DHistory {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
return DHistory(
|
|
||||||
url = array[0].jsonPrimitive.content,
|
|
||||||
lastRead = array[1].jsonPrimitive.long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.add
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonArray {
|
|
||||||
add(value.url)
|
|
||||||
add(value.title)
|
|
||||||
add(value.source)
|
|
||||||
add(value.viewer_flags)
|
|
||||||
add(value.chapter_flags)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a manga impl and cast as T so that the serializer accepts it
|
|
||||||
return MangaImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val array = decoder.decodeJsonElement().jsonArray
|
|
||||||
url = array[0].jsonPrimitive.content
|
|
||||||
title = array[1].jsonPrimitive.content
|
|
||||||
source = array[2].jsonPrimitive.long
|
|
||||||
viewer_flags = array[3].jsonPrimitive.int
|
|
||||||
chapter_flags = array[4].jsonPrimitive.int
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a manga and manga impl
|
|
||||||
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
|
||||||
|
|
||||||
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
|
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlinx.serialization.json.JsonDecoder
|
|
||||||
import kotlinx.serialization.json.JsonEncoder
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
|
||||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: T) {
|
|
||||||
encoder as JsonEncoder
|
|
||||||
encoder.encodeJsonElement(
|
|
||||||
buildJsonObject {
|
|
||||||
put(TITLE, value.title)
|
|
||||||
put(SYNC, value.sync_id)
|
|
||||||
put(MEDIA, value.media_id)
|
|
||||||
put(LIBRARY, value.library_id)
|
|
||||||
put(LAST_READ, value.last_chapter_read)
|
|
||||||
put(TRACKING_URL, value.tracking_url)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun deserialize(decoder: Decoder): T {
|
|
||||||
// make a track impl and cast as T so that the serializer accepts it
|
|
||||||
return TrackImpl().apply {
|
|
||||||
decoder as JsonDecoder
|
|
||||||
val jsonObject = decoder.decodeJsonElement().jsonObject
|
|
||||||
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
|
||||||
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
|
||||||
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
|
||||||
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
|
||||||
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
|
|
||||||
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for serialization of a track and track impl
|
|
||||||
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
|
||||||
|
|
||||||
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json values
|
||||||
|
*/
|
||||||
|
object Backup {
|
||||||
|
const val CURRENT_VERSION = 2
|
||||||
|
const val MANGA = "manga"
|
||||||
|
const val MANGAS = "mangas"
|
||||||
|
const val TRACK = "track"
|
||||||
|
const val CHAPTERS = "chapters"
|
||||||
|
const val CATEGORIES = "categories"
|
||||||
|
const val EXTENSIONS = "extensions"
|
||||||
|
const val HISTORY = "history"
|
||||||
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
object CategoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<CategoryImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.name)
|
||||||
|
value(it.order)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val category = CategoryImpl()
|
||||||
|
category.name = nextString()
|
||||||
|
category.order = nextInt()
|
||||||
|
endArray()
|
||||||
|
category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
object ChapterTypeAdapter {
|
||||||
|
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<ChapterImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||||
|
beginObject()
|
||||||
|
name(URL)
|
||||||
|
value(it.url)
|
||||||
|
if (it.read) {
|
||||||
|
name(READ)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.bookmark) {
|
||||||
|
name(BOOKMARK)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.last_page_read != 0) {
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_page_read)
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val chapter = ChapterImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
when (nextName()) {
|
||||||
|
URL -> chapter.url = nextString()
|
||||||
|
READ -> chapter.read = nextInt() == 1
|
||||||
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
LAST_READ -> chapter.last_page_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<DHistory> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.lastRead != 0L) {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.lastRead)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val url = nextString()
|
||||||
|
val lastRead = nextLong()
|
||||||
|
endArray()
|
||||||
|
DHistory(url, lastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
object MangaTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MangaImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.title)
|
||||||
|
value(it.source)
|
||||||
|
value(it.viewer)
|
||||||
|
value(it.chapter_flags)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val manga = MangaImpl()
|
||||||
|
manga.url = nextString()
|
||||||
|
manga.title = nextString()
|
||||||
|
manga.source = nextLong()
|
||||||
|
manga.viewer = nextInt()
|
||||||
|
manga.chapter_flags = nextInt()
|
||||||
|
endArray()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginObject()
|
||||||
|
name(TITLE)
|
||||||
|
value(it.title)
|
||||||
|
name(SYNC)
|
||||||
|
value(it.sync_id)
|
||||||
|
name(MEDIA)
|
||||||
|
value(it.media_id)
|
||||||
|
name(LIBRARY)
|
||||||
|
value(it.library_id)
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_chapter_read)
|
||||||
|
name(TRACKING_URL)
|
||||||
|
value(it.tracking_url)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val track = TrackImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
when (nextName()) {
|
||||||
|
TITLE -> track.title = nextString()
|
||||||
|
SYNC -> track.sync_id = nextInt()
|
||||||
|
MEDIA -> track.media_id = nextInt()
|
||||||
|
LIBRARY -> track.library_id = nextLong()
|
||||||
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
TRACKING_URL -> track.tracking_url = nextString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
vendored
Normal file → Executable file
@ -2,20 +2,26 @@ package eu.kanade.tachiyomi.data.cache
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import kotlinx.serialization.decodeFromString
|
import java.io.File
|
||||||
import kotlinx.serialization.encodeToString
|
import java.io.IOException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to create chapter cache
|
* Class used to create chapter cache
|
||||||
@ -37,25 +43,37 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
/** The number of values per cache entry. Must be positive. */
|
/** The number of values per cache entry. Must be positive. */
|
||||||
const val PARAMETER_VALUE_COUNT = 1
|
const val PARAMETER_VALUE_COUNT = 1
|
||||||
|
|
||||||
/** The maximum number of bytes this cache should use to store. */
|
|
||||||
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
/** Google Json class used for parsing JSON files. */
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
// --> EH
|
||||||
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
// --> EH
|
||||||
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
private var diskCache = setupDiskCache(prefs.eh_cacheSize().get().toLong())
|
||||||
PARAMETER_APP_VERSION,
|
|
||||||
PARAMETER_VALUE_COUNT,
|
init {
|
||||||
PARAMETER_CACHE_SIZE
|
prefs.eh_cacheSize().asFlow()
|
||||||
)
|
.onEach {
|
||||||
|
// Save old cache for destruction later
|
||||||
|
val oldCache = diskCache
|
||||||
|
diskCache = setupDiskCache(it.toLong())
|
||||||
|
oldCache.close()
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns directory of cache.
|
* Returns directory of cache.
|
||||||
*/
|
*/
|
||||||
private val cacheDir: File
|
val cacheDir: File
|
||||||
get() = diskCache.directory
|
get() = diskCache.directory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,19 +88,55 @@ class ChapterCache(private val context: Context) {
|
|||||||
val readableSize: String
|
val readableSize: String
|
||||||
get() = Formatter.formatFileSize(context, realSize)
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
|
// --> EH
|
||||||
|
// Cache size is in MB
|
||||||
|
private fun setupDiskCache(cacheSize: Long): DiskLruCache {
|
||||||
|
return DiskLruCache.open(
|
||||||
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
|
PARAMETER_APP_VERSION,
|
||||||
|
PARAMETER_VALUE_COUNT,
|
||||||
|
cacheSize * 1024 * 1024
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// <-- EH
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove file from cache.
|
||||||
|
*
|
||||||
|
* @param file name of file "md5.0".
|
||||||
|
* @return status of deletion for the file.
|
||||||
|
*/
|
||||||
|
fun removeFileFromCache(file: String): Boolean {
|
||||||
|
// Make sure we don't delete the journal file (keeps track of cache).
|
||||||
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Remove the extension from the file to get the key of the cache
|
||||||
|
val key = file.substringBeforeLast(".")
|
||||||
|
// Remove file from cache.
|
||||||
|
diskCache.remove(key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page list from cache.
|
* Get page list from cache.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return the list of pages.
|
* @return an observable of the list of pages.
|
||||||
*/
|
*/
|
||||||
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||||
|
return Observable.fromCallable {
|
||||||
// Get the key for the chapter.
|
// Get the key for the chapter.
|
||||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
|
|
||||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
return diskCache.get(key).use {
|
diskCache.get(key).use {
|
||||||
json.decodeFromString(it.getString(0))
|
gson.fromJson<List<Page>>(it.getString(0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +148,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||||
// Convert list of pages to json string.
|
// Convert list of pages to json string.
|
||||||
val cachedValue = json.encodeToString(pages)
|
val cachedValue = gson.toJson(pages)
|
||||||
|
|
||||||
// Initialize the editor (edits the values for an entry).
|
// Initialize the editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
@ -174,38 +228,6 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear(): Int {
|
|
||||||
var deletedFiles = 0
|
|
||||||
cacheDir.listFiles()?.forEach {
|
|
||||||
if (removeFileFromCache(it.name)) {
|
|
||||||
deletedFiles++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deletedFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove file from cache.
|
|
||||||
*
|
|
||||||
* @param file name of file "md5.0".
|
|
||||||
* @return status of deletion for the file.
|
|
||||||
*/
|
|
||||||
private fun removeFileFromCache(file: String): Boolean {
|
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
|
||||||
if (file == "journal" || file.startsWith("journal.")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
// Remove the extension from the file to get the key of the cache
|
|
||||||
val key = file.substringBeforeLast(".")
|
|
||||||
// Remove file from cache.
|
|
||||||
diskCache.remove(key)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getKey(chapter: Chapter): String {
|
private fun getKey(chapter: Chapter): String {
|
||||||
return "${chapter.manga_id}${chapter.url}"
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
}
|
}
|
||||||
|
8
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
vendored
Normal file → Executable file
@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import coil.imageLoader
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -100,13 +99,6 @@ class CoverCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear coil's memory cache.
|
|
||||||
*/
|
|
||||||
fun clearMemoryCache() {
|
|
||||||
context.imageLoader.memoryCache.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
return context.getExternalFilesDir(dir)
|
return context.getExternalFilesDir(dir)
|
||||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.size.Size
|
|
||||||
import okio.buffer
|
|
||||||
import okio.source
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = ByteArrayInputStream(data.array()).source().buffer(),
|
|
||||||
mimeType = null,
|
|
||||||
dataSource = DataSource.MEMORY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun key(data: ByteBuffer): String? = null
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.network.HttpException
|
|
||||||
import coil.request.get
|
|
||||||
import coil.size.Size
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import okhttp3.CacheControl
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import okio.source
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
|
||||||
*
|
|
||||||
* Available request parameter:
|
|
||||||
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
|
||||||
*/
|
|
||||||
class MangaCoverFetcher : Fetcher<Manga> {
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
|
||||||
|
|
||||||
override fun key(data: Manga): String? {
|
|
||||||
if (data.thumbnail_url.isNullOrBlank()) return null
|
|
||||||
return data.thumbnail_url!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
|
||||||
// Use custom cover if exists
|
|
||||||
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
|
||||||
val customCoverFile = coverCache.getCustomCoverFile(data)
|
|
||||||
if (useCustomCover && customCoverFile.exists()) {
|
|
||||||
return fileLoader(customCoverFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cover = data.thumbnail_url
|
|
||||||
return when (getResourceType(cover)) {
|
|
||||||
Type.URL -> httpLoader(data, options)
|
|
||||||
Type.File -> fileLoader(data)
|
|
||||||
null -> error("Invalid image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
|
||||||
// Only cache separately if it's a library item
|
|
||||||
val coverCacheFile = if (manga.favorite) {
|
|
||||||
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
|
||||||
return fileLoader(coverCacheFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
body.close()
|
|
||||||
throw HttpException(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
|
||||||
coverCacheFile.parentFile?.mkdirs()
|
|
||||||
if (coverCacheFile.exists()) {
|
|
||||||
coverCacheFile.delete()
|
|
||||||
}
|
|
||||||
coverCacheFile.sink().buffer().use { output ->
|
|
||||||
output.writeAll(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SourceResult(
|
|
||||||
source = body.source(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
|
||||||
val call = getCall(manga, options)
|
|
||||||
val response = call.await()
|
|
||||||
return response to checkNotNull(response.body) { "Null response source" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
|
||||||
if (source != null) {
|
|
||||||
it.headers(source.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkRead = options.networkCachePolicy.readEnabled
|
|
||||||
val diskRead = options.diskCachePolicy.readEnabled
|
|
||||||
when {
|
|
||||||
!networkRead && diskRead -> {
|
|
||||||
it.cacheControl(CacheControl.FORCE_CACHE)
|
|
||||||
}
|
|
||||||
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
|
||||||
it.cacheControl(CacheControl.FORCE_NETWORK)
|
|
||||||
} else {
|
|
||||||
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
!networkRead && !diskRead -> {
|
|
||||||
// This causes the request to fail with a 504 Unsatisfiable Request.
|
|
||||||
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
|
||||||
return client.newCall(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
|
||||||
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fileLoader(file: File): FetchResult {
|
|
||||||
return SourceResult(
|
|
||||||
source = file.source().buffer(),
|
|
||||||
mimeType = "image/*",
|
|
||||||
dataSource = DataSource.DISK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getResourceType(cover: String?): Type? {
|
|
||||||
return when {
|
|
||||||
cover.isNullOrEmpty() -> null
|
|
||||||
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
|
||||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class Type {
|
|
||||||
File, URL
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val USE_CUSTOM_COVER = "use_custom_cover"
|
|
||||||
|
|
||||||
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
|
||||||
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import coil.bitmap.BitmapPool
|
|
||||||
import coil.decode.DecodeResult
|
|
||||||
import coil.decode.Decoder
|
|
||||||
import coil.decode.Options
|
|
||||||
import coil.size.Size
|
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import okio.BufferedSource
|
|
||||||
import tachiyomi.decoder.ImageDecoder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
|
||||||
*/
|
|
||||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
|
||||||
|
|
||||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
|
||||||
val type = source.peek().inputStream().use {
|
|
||||||
ImageUtil.findImageType(it)
|
|
||||||
}
|
|
||||||
return when (type) {
|
|
||||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
|
||||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun decode(
|
|
||||||
pool: BitmapPool,
|
|
||||||
source: BufferedSource,
|
|
||||||
size: Size,
|
|
||||||
options: Options
|
|
||||||
): DecodeResult {
|
|
||||||
val decoder = source.use {
|
|
||||||
ImageDecoder.newInstance(it.inputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
|
||||||
|
|
||||||
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
|
||||||
decoder.recycle()
|
|
||||||
|
|
||||||
check(bitmap != null) { "Failed to decode image." }
|
|
||||||
|
|
||||||
return DecodeResult(
|
|
||||||
drawable = bitmap.toDrawable(resources),
|
|
||||||
isSampled = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
18
app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
Normal file → Executable file
@ -21,13 +21,22 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import exh.metadata.sql.mappers.SearchMetadataTypeMapping
|
||||||
|
import exh.metadata.sql.mappers.SearchTagTypeMapping
|
||||||
|
import exh.metadata.sql.mappers.SearchTitleTypeMapping
|
||||||
|
import exh.metadata.sql.models.SearchMetadata
|
||||||
|
import exh.metadata.sql.models.SearchTag
|
||||||
|
import exh.metadata.sql.models.SearchTitle
|
||||||
|
import exh.metadata.sql.queries.SearchMetadataQueries
|
||||||
|
import exh.metadata.sql.queries.SearchTagQueries
|
||||||
|
import exh.metadata.sql.queries.SearchTitleQueries
|
||||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@ -42,7 +51,14 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
|
// EXH -->
|
||||||
|
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||||
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
|
// EXH <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
|
||||||
|
fun lowLevel() = db.lowLevel()
|
||||||
}
|
}
|
||||||
|
0
app/src/main/java/eu/kanade/tachiyomi/data/database/DbExtensions.kt
Normal file → Executable file
67
app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
Normal file → Executable file
@ -7,7 +7,11 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MergedTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
|
import exh.metadata.sql.tables.SearchTitleTable
|
||||||
|
|
||||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||||
|
|
||||||
@ -20,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 12
|
const val DATABASE_VERSION = 2 // [SY]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -30,6 +34,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
execSQL(HistoryTable.createTableQuery)
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
// EXH -->
|
||||||
|
execSQL(SearchMetadataTable.createTableQuery)
|
||||||
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
|
// EXH <--
|
||||||
|
// AZ -->
|
||||||
|
execSQL(MergedTable.createTableQuery)
|
||||||
|
// AZ <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
@ -37,54 +49,23 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
|
// EXH -->
|
||||||
|
db.execSQL(SearchMetadataTable.createUploaderIndexQuery)
|
||||||
|
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
|
||||||
|
db.execSQL(SearchTagTable.createMangaIdIndexQuery)
|
||||||
|
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery)
|
||||||
|
db.execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
|
db.execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
|
// EXH <--
|
||||||
|
// AZ -->
|
||||||
|
execSQL(MergedTable.createIndexQuery)
|
||||||
|
// AZ <--
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
|
||||||
|
|
||||||
// Fix kissmanga covers after supporting cloudflare
|
|
||||||
db.execSQL(
|
|
||||||
"""UPDATE mangas SET thumbnail_url =
|
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (oldVersion < 3) {
|
|
||||||
// Initialize history tables
|
|
||||||
db.execSQL(HistoryTable.createTableQuery)
|
|
||||||
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 4) {
|
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 5) {
|
|
||||||
db.execSQL(ChapterTable.addScanlator)
|
|
||||||
}
|
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.execSQL(TrackTable.addTrackingUrl)
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.execSQL(TrackTable.addLibraryId)
|
|
||||||
}
|
|
||||||
if (oldVersion < 8) {
|
|
||||||
db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index")
|
|
||||||
db.execSQL(MangaTable.createLibraryIndexQuery)
|
|
||||||
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 9) {
|
|
||||||
db.execSQL(TrackTable.addStartDate)
|
|
||||||
db.execSQL(TrackTable.addFinishDate)
|
|
||||||
}
|
|
||||||
if (oldVersion < 10) {
|
|
||||||
db.execSQL(MangaTable.addCoverLastModified)
|
db.execSQL(MangaTable.addCoverLastModified)
|
||||||
}
|
}
|
||||||
if (oldVersion < 11) {
|
|
||||||
db.execSQL(MangaTable.addDateAdded)
|
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
|
||||||
}
|
|
||||||
if (oldVersion < 12) {
|
|
||||||
db.execSQL(MangaTable.addNextUpdateCol)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
0
app/src/main/java/eu/kanade/tachiyomi/data/database/DbProvider.kt
Normal file → Executable file
21
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/CategoryTypeMapping.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
|||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_MANGA_ORDER
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||||
@ -35,13 +36,14 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Category) =
|
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||||
contentValuesOf(
|
put(COL_ID, obj.id)
|
||||||
COL_ID to obj.id,
|
put(COL_NAME, obj.name)
|
||||||
COL_NAME to obj.name,
|
put(COL_ORDER, obj.order)
|
||||||
COL_ORDER to obj.order,
|
put(COL_FLAGS, obj.flags)
|
||||||
COL_FLAGS to obj.flags
|
val orderString = obj.mangaOrder.joinToString("/")
|
||||||
)
|
put(COL_MANGA_ORDER, orderString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
@ -51,6 +53,9 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
|
|||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||||
|
|
||||||
|
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
||||||
|
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() } ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/ChapterTypeMapping.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -43,21 +43,20 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Chapter) =
|
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||||
contentValuesOf(
|
put(COL_ID, obj.id)
|
||||||
COL_ID to obj.id,
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
COL_MANGA_ID to obj.manga_id,
|
put(COL_URL, obj.url)
|
||||||
COL_URL to obj.url,
|
put(COL_NAME, obj.name)
|
||||||
COL_NAME to obj.name,
|
put(COL_READ, obj.read)
|
||||||
COL_READ to obj.read,
|
put(COL_SCANLATOR, obj.scanlator)
|
||||||
COL_SCANLATOR to obj.scanlator,
|
put(COL_BOOKMARK, obj.bookmark)
|
||||||
COL_BOOKMARK to obj.bookmark,
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
COL_DATE_FETCH to obj.date_fetch,
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
COL_DATE_UPLOAD to obj.date_upload,
|
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
put(COL_SOURCE_ORDER, obj.source_order)
|
||||||
COL_SOURCE_ORDER to obj.source_order
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
15
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/HistoryTypeMapping.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -35,13 +35,12 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: History) =
|
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||||
contentValuesOf(
|
put(COL_ID, obj.id)
|
||||||
COL_ID to obj.id,
|
put(COL_CHAPTER_ID, obj.chapter_id)
|
||||||
COL_CHAPTER_ID to obj.chapter_id,
|
put(COL_LAST_READ, obj.last_read)
|
||||||
COL_LAST_READ to obj.last_read,
|
put(COL_TIME_READ, obj.time_read)
|
||||||
COL_TIME_READ to obj.time_read
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
13
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaCategoryTypeMapping.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -33,12 +33,11 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: MangaCategory) =
|
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||||
contentValuesOf(
|
put(COL_ID, obj.id)
|
||||||
COL_ID to obj.id,
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
COL_MANGA_ID to obj.manga_id,
|
put(COL_CATEGORY_ID, obj.category_id)
|
||||||
COL_CATEGORY_ID to obj.category_id
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
47
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt
Normal file → Executable file
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.mappers
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.core.content.contentValuesOf
|
|
||||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
@ -15,14 +15,12 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -49,27 +47,24 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
.whereArgs(obj.id)
|
.whereArgs(obj.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun mapToContentValues(obj: Manga) =
|
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||||
contentValuesOf(
|
put(COL_ID, obj.id)
|
||||||
COL_ID to obj.id,
|
put(COL_SOURCE, obj.source)
|
||||||
COL_SOURCE to obj.source,
|
put(COL_URL, obj.url)
|
||||||
COL_URL to obj.url,
|
put(COL_ARTIST, obj.artist)
|
||||||
COL_ARTIST to obj.artist,
|
put(COL_AUTHOR, obj.author)
|
||||||
COL_AUTHOR to obj.author,
|
put(COL_DESCRIPTION, obj.description)
|
||||||
COL_DESCRIPTION to obj.description,
|
put(COL_GENRE, obj.genre)
|
||||||
COL_GENRE to obj.genre,
|
put(COL_TITLE, obj.title)
|
||||||
COL_TITLE to obj.title,
|
put(COL_STATUS, obj.status)
|
||||||
COL_STATUS to obj.status,
|
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
put(COL_FAVORITE, obj.favorite)
|
||||||
COL_FAVORITE to obj.favorite,
|
put(COL_LAST_UPDATE, obj.last_update)
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
put(COL_INITIALIZED, obj.initialized)
|
||||||
COL_NEXT_UPDATE to obj.next_update,
|
put(COL_VIEWER, obj.viewer)
|
||||||
COL_INITIALIZED to obj.initialized,
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
COL_VIEWER to obj.viewer_flags,
|
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
}
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
|
||||||
COL_DATE_ADDED to obj.date_added
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
@ -86,12 +81,10 @@ interface BaseMangaGetResolver {
|
|||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|