Compare commits
2380 Commits
Author | SHA1 | Date | |
---|---|---|---|
45ca470789 | |||
a3b1690d38 | |||
a3bad75899 | |||
d1aaafbfff | |||
93d4af99bf | |||
c950595fe3 | |||
8ffd3a8ed2 | |||
b6e246c6b2 | |||
59859e124f | |||
2bb7a33bc3 | |||
c2b8fea291 | |||
08ab7f6aa0 | |||
560f0bba5c | |||
722437a022 | |||
8a44b1dabe | |||
b39191ff50 | |||
9814d20404 | |||
6664dfb048 | |||
3133a63cf8 | |||
c9c0f3d014 | |||
ff66f307dd | |||
e048d66f74 | |||
66e3fa7df8 | |||
019a0f31c7 | |||
749c2071af | |||
322d66d282 | |||
aa98cd0da0 | |||
c8316c7254 | |||
6b9180844d | |||
c0e4863229 | |||
2be9871d05 | |||
776f6a9a16 | |||
10163aab21 | |||
60b2a4ea9d | |||
56e1e3e205 | |||
0f805cd45e | |||
1d7c692e89 | |||
38bc8ec6b4 | |||
2154e3aa2d | |||
56c19e57a9 | |||
d548c690d6 | |||
3fa70dade3 | |||
368c30a2cc | |||
5539e4591f | |||
781971ee81 | |||
1140316d1b | |||
cf6c48744a | |||
eed6db8e92 | |||
858664bfd7 | |||
eceac4d6e3 | |||
47a172df1f | |||
f2c0732c40 | |||
682fae12b6 | |||
a150762c63 | |||
2695bdddf8 | |||
c9b1a425a7 | |||
122b2b1a8e | |||
2351c1b426 | |||
558c4ada06 | |||
779fd9c61a | |||
0b5128dfd1 | |||
c0519e8670 | |||
fd545db1bd | |||
6991c224b2 | |||
7dc70c9eab | |||
e32445f2cf | |||
8aa6486bf7 | |||
d21c147203 | |||
9b10e851d1 | |||
6675508b24 | |||
7310ec4fe4 | |||
b1ce3693ed | |||
deb1ed5623 | |||
0902d7cca9 | |||
95ec903862 | |||
2ab6af6471 | |||
9493577de2 | |||
837ce62844 | |||
2860bbfb12 | |||
a2b1acd70f | |||
f1350bc33e | |||
6af0eb4068 | |||
538c168641 | |||
832a4fa68e | |||
ccb727529d | |||
ed41604f56 | |||
9f05d563f9 | |||
72d114d46a | |||
99b96d80d0 | |||
67418ba853 | |||
60755d0c26 | |||
a689e4e041 | |||
f5aa36c787 | |||
f8d82cb052 | |||
980feb6c96 | |||
e7d6605490 | |||
7a476abb53 | |||
19581792fc | |||
aadc6a56cd | |||
b88e444cbc | |||
6c792d2821 | |||
9afb445620 | |||
efc951191d | |||
a249373bf5 | |||
e1eb030b18 | |||
6aea0f48ed | |||
e637f22540 | |||
842295348e | |||
2992a0f4d8 | |||
e8f5963a57 | |||
4cbe497770 | |||
76a53097b1 | |||
0904692f15 | |||
65bacd288b | |||
11ab3b2c2e | |||
812368e332 | |||
cf39ae0000 | |||
7194f65203 | |||
4b78ff324d | |||
79ccfcd553 | |||
2df6a4dde8 | |||
e88cbc2769 | |||
25d1c40cda | |||
969b57ade9 | |||
bddeb86223 | |||
b5986b509e | |||
9d2adcd512 | |||
fb3756420b | |||
2eab43a669 | |||
caeab0a63b | |||
7c69b1b649 | |||
3784d1a8f2 | |||
458e761b45 | |||
371b0b2132 | |||
5d1ca64768 | |||
972a595c74 | |||
a3c598a3e1 | |||
9ce8c5c160 | |||
79bbc99882 | |||
31867362dc | |||
3bce07e873 | |||
766f9e37b5 | |||
a9bed90d02 | |||
01ad405dd2 | |||
8cef35f4a0 | |||
274f0edd76 | |||
88aea311f8 | |||
477aedbffa | |||
b898442fe3 | |||
528c1b90c2 | |||
205c1e5170 | |||
004e1c98ee | |||
7641bb4d0d | |||
687f3d48ea | |||
da5f10a2f1 | |||
64050e8266 | |||
791a7d5a01 | |||
13930d3706 | |||
2769e27a2a | |||
76c795d0d0 | |||
b20bced3ca | |||
4f2da9a78f | |||
8e0ba3650b | |||
76f6fe4601 | |||
ca1373f36b | |||
fc6c2e083d | |||
c0789cd6ba | |||
af47103707 | |||
c466baaa25 | |||
670294a427 | |||
21ddae6a86 | |||
9f260c3513 | |||
18061d1077 | |||
381c061ebc | |||
d37341d7d0 | |||
a5098e5b5b | |||
b55d394a1f | |||
5e2e177aa9 | |||
86e59977de | |||
66baf01e43 | |||
7a33e198dc | |||
4b493ebbaf | |||
565e8cf00b | |||
738a3999b4 | |||
6f4b84c8fb | |||
29ab99aa1f | |||
d53719b79e | |||
f10fe8bf02 | |||
d7cfe1990c | |||
8bedc8f456 | |||
d9000f6fd1 | |||
50c7b32b00 | |||
78072ad285 | |||
437a34b5dc | |||
3ebea4c305 | |||
8fe315c354 | |||
b10b13a339 | |||
5b5ea5ab8a | |||
1a230b3900 | |||
e90b0aaf8b | |||
fe7c7e72f5 | |||
9ba11a585f | |||
9920ff617b | |||
3f1355c413 | |||
4929e66ecc | |||
02e370c2d8 | |||
4c31e3fc5f | |||
15f49b39b8 | |||
8c82b766e3 | |||
4c8665c9f0 | |||
ba67781431 | |||
4ef25c75b7 | |||
3aafc671f8 | |||
967df6f7a3 | |||
22518f173f | |||
64bdfabbd8 | |||
c8c65ab7b1 | |||
19cd28b66b | |||
4a136ef2aa | |||
9e7a53cb90 | |||
19a7f37efa | |||
c3084ac43a | |||
159146e197 | |||
67ddf4a5b8 | |||
4b9b53a9b8 | |||
e6f025a9fb | |||
aa607e0ecb | |||
65b32ddeb2 | |||
5e9bdc2690 | |||
9ce994168a | |||
748a720199 | |||
8db34eb3dd | |||
b657bba96e | |||
dbaac69fad | |||
b6a1e89535 | |||
cce919750a | |||
9376b223bb | |||
6f047fb5aa | |||
3e6b0117fd | |||
421dfb4a2d | |||
abaca6e676 | |||
8bab1d9798 | |||
13d31669ac | |||
c1dfdeb500 | |||
dda7e677a5 | |||
b1fb401f63 | |||
885ace111e | |||
885552b792 | |||
4f02872a84 | |||
ecec1bd102 | |||
0c07e05a2b | |||
060f0682f4 | |||
88032e11df | |||
493c8b0943 | |||
af2ef0621a | |||
095461e31b | |||
3ddd1033c3 | |||
912687ac78 | |||
40a9595012 | |||
12ff37d052 | |||
4857073f30 | |||
cdbefd9191 | |||
2e9d89574d | |||
569c99496b | |||
ea3b8767de | |||
8e8c30c1eb | |||
d921ba81c8 | |||
ad9f646102 | |||
2ef277bcef | |||
9e396e1624 | |||
9708d84e60 | |||
4efc195548 | |||
0d15cbe334 | |||
e381d9fc8e | |||
85ed7a7457 | |||
b8a98ef5e4 | |||
c8fa90f473 | |||
b47ee8857b | |||
131dfa62c4 | |||
6a5af438dd | |||
ccc0a61158 | |||
e990ad25eb | |||
98a4d1e763 | |||
f762598c5c | |||
ec56c27071 | |||
eb0e0a1952 | |||
01a837fde6 | |||
b9488645d4 | |||
7a94b477cb | |||
99710b45d1 | |||
3813743e3d | |||
9bb2334b69 | |||
7e73ede47a | |||
b0106aa420 | |||
33e5fea96c | |||
f0a1dcd120 | |||
26d5a87bef | |||
52ae208df3 | |||
34aaa7fb0a | |||
a8c784355c | |||
0aed93becf | |||
1ea0804209 | |||
a52fbb012a | |||
2dc47352f8 | |||
e95a5be21d | |||
abd69d4f91 | |||
d749e309f8 | |||
e4d075fb91 | |||
71c6c71081 | |||
d2b14bcfc4 | |||
2c04c81bd1 | |||
dd66c83c50 | |||
9e51d82154 | |||
bdc441a5be | |||
2dcb73700b | |||
ee01686ae4 | |||
9a55cf880e | |||
6742cdeb8b | |||
c37377bffa | |||
76147a9be7 | |||
bf22e69250 | |||
49693934cf | |||
e6a63ee5b2 | |||
08dc57fd02 | |||
cede590696 | |||
c401915fb5 | |||
2a202bd510 | |||
dcd8ed08fc | |||
d3ebedeef2 | |||
d2e2ebbe45 | |||
0cef05dd89 | |||
a443dc3040 | |||
4e6cc013e5 | |||
0c65d54d89 | |||
ccd0e0cdfe | |||
9278ca3f5e | |||
d7a70b962b | |||
fff0f841fa | |||
8ba426350f | |||
8ef548032f | |||
5452e29840 | |||
148f8e6d11 | |||
13a5662a84 | |||
6713a7ae3c | |||
226ad13061 | |||
88ee86b7ef | |||
4bc2288806 | |||
a928d9fa0b | |||
d8f4e6b45f | |||
5ef5087406 | |||
135c371d88 | |||
966c196f4a | |||
dc43e41896 | |||
4809d06d04 | |||
9f7fda0bc5 | |||
1f67695713 | |||
66ef1a8206 | |||
beaffc3870 | |||
8536ecb611 | |||
d7a89b0f8c | |||
943081e80d | |||
3f007a1edd | |||
e33cacf6a4 | |||
23fe848a35 | |||
fa5d2276c0 | |||
d353a3457d | |||
b363b9fc1a | |||
1920568057 | |||
763da19c9d | |||
1813dbbf59 | |||
339169b624 | |||
93960315d9 | |||
479eb1ba71 | |||
962d8e5fd2 | |||
fefd4c0b26 | |||
40639c0933 | |||
7401673ac1 | |||
41f759dafe | |||
73dcc7bcb1 | |||
d8b1f60581 | |||
16fc58bd16 | |||
68df2f4ce7 | |||
367932de69 | |||
3da08cbcbf | |||
68efc0c42f | |||
f5b01b0ca2 | |||
5733429682 | |||
c6d29fc19b | |||
d9a12d79b0 | |||
963cf4c996 | |||
0ef073669a | |||
ed1123feb0 | |||
c2114bbd4f | |||
fedb1d2590 | |||
eef39b75a6 | |||
eca593ac36 | |||
a1917b8c81 | |||
ec6dba12bd | |||
da0671ad62 | |||
04d83e9a6a | |||
2cb7624953 | |||
e6ace844b6 | |||
f2f6628693 | |||
1c33032721 | |||
b3f5f13c39 | |||
a5339969c9 | |||
1681437206 | |||
2eaf083eee | |||
3645d19135 | |||
3b4b1185e2 | |||
406c5bde11 | |||
75d1913aaf | |||
eb254d9c56 | |||
4e633b8936 | |||
e99a27e382 | |||
c8a6a2653f | |||
0ea0eba4f0 | |||
7f88b56d8b | |||
789421c7a0 | |||
d44503cb19 | |||
0258422527 | |||
47327d840d | |||
d0f1a33744 | |||
c54b8e62d7 | |||
3dc738f28c | |||
ca75400467 | |||
65091c05c9 | |||
52e846f3b6 | |||
ce22b2c29a | |||
361b0284fa | |||
b8947a1c50 | |||
8d1effa0e8 | |||
096a9f4cbf | |||
18712b166f | |||
4605e14729 | |||
a768280d82 | |||
cbb8f25645 | |||
24ff7ff67c | |||
eee0bd6cf4 | |||
f176a5179a | |||
381ba86e3c | |||
f4be8e28ca | |||
e17605f8d9 | |||
e06a488af8 | |||
6dc8bfed8d | |||
f642f23366 | |||
c041410f61 | |||
ff5f13eafe | |||
749b240897 | |||
9207762ade | |||
0a22950ad3 | |||
0fcd404b4f | |||
150ea29a70 | |||
f9baff0e90 | |||
6ad3fcb91d | |||
395ca3630c | |||
e9fbdb660b | |||
526e029ebb | |||
763c4522b7 | |||
32e3ac63ed | |||
90f31aab38 | |||
3d44feaf2c | |||
f5a44245e9 | |||
7e7eb9f39f | |||
6c9b982104 | |||
a106104027 | |||
7753161332 | |||
ec9d592cf1 | |||
31015504f4 | |||
bd40ec527d | |||
3899938b25 | |||
ca7373c28b | |||
390bdfa93d | |||
ac2df87954 | |||
0b73f8b1ef | |||
812fcec9f0 | |||
ec7297f8c2 | |||
0fdb19c07d | |||
3bc07b4753 | |||
0b8b13d0bb | |||
4c5bf9bc8e | |||
9f53109414 | |||
bccb1229c8 | |||
40ab3fe0a6 | |||
4a99118cce | |||
58ba29fa16 | |||
54cfb2acdf | |||
0bf14fd31c | |||
7dd9a0211b | |||
3d43473bf8 | |||
2194c4ba28 | |||
744a4f8f47 | |||
67d91f7b69 | |||
bf5065d16b | |||
3e837f8781 | |||
77d378ccd1 | |||
1a542bae71 | |||
e3ed12b5d2 | |||
759795940b | |||
a23d5ab734 | |||
6ee69ef430 | |||
3edf17d322 | |||
8c2b2f99bc | |||
73dc51b3f6 | |||
9a082d4df1 | |||
f430b6f853 | |||
78a352541a | |||
a0f5633094 | |||
0af81c7d05 | |||
52e82b3548 | |||
f05b99ec1f | |||
194897bf3c | |||
7cf26363c8 | |||
3d1dec4c05 | |||
af1935d2e4 | |||
3c4bc17065 | |||
333d1c1ad9 | |||
4e027cec71 | |||
39ae84301a | |||
3bf14623ad | |||
ac8f2923e5 | |||
e9d3b75e2b | |||
e6bc181e7a | |||
a2ece82197 | |||
259946cf0a | |||
4fdb4f14a8 | |||
914f5e569b | |||
9b4ffd1cd5 | |||
ed87dd89a1 | |||
3b41a78e76 | |||
0fccbbc0ca | |||
067627b51a | |||
09816ed5b6 | |||
b457cdb0c2 | |||
5fd1dec347 | |||
647391ef73 | |||
ed029c52ae | |||
102a372df9 | |||
d4ffb09a8b | |||
6ba052d2af | |||
57b63f43f5 | |||
2fb0969c75 | |||
3357e878a5 | |||
471d5d62d5 | |||
e810b343cf | |||
620be2617a | |||
035038a0b6 | |||
b8ffb87f01 | |||
39e1e11f99 | |||
5f9df78ab0 | |||
a00d11701f | |||
1cf74a5396 | |||
8cd27a199d | |||
772929b5c6 | |||
c4ca3606ad | |||
9e830f1c55 | |||
ee8c71c14a | |||
39bd823651 | |||
a9d16fad34 | |||
1da169319d | |||
2bb1eea2be | |||
9cb45b92e1 | |||
4a8d5098da | |||
d875d5ef74 | |||
fc4e290c49 | |||
ccc198e081 | |||
9f0ed77423 | |||
bb3e616890 | |||
573f3a392a | |||
830a834ea6 | |||
e4ea5d0344 | |||
97aed045e6 | |||
46b01c6134 | |||
e208fa4020 | |||
6b71264482 | |||
5723c184b1 | |||
530daeaa3a | |||
dd1b5c7ea7 | |||
3cffc6890d | |||
04d44f19f5 | |||
d46a742a43 | |||
a94fd24fa9 | |||
8a4c4c346a | |||
dc54299e24 | |||
436253dd63 | |||
29feee0095 | |||
63f3180dff | |||
6b3b98cf57 | |||
1442e2b53e | |||
521ebf0678 | |||
a20874f6a1 | |||
40776bdc8d | |||
f853158e6b | |||
c9035b5df9 | |||
150132f4dd | |||
fb97ac47bc | |||
5b53b90495 | |||
c6513d4450 | |||
ce6848b3c0 | |||
d86d861e4b | |||
3b45fcdb21 | |||
3d1250f2f8 | |||
eaf1ef831a | |||
b4c7992726 | |||
03baa21185 | |||
694de99a3f | |||
8383f4fb7b | |||
eb3fff6c51 | |||
4ff3f0bcba | |||
788ea052fc | |||
08ba805bbd | |||
dbd14c6dac | |||
5977fca6b9 | |||
fcf596d36b | |||
dabca5f09e | |||
018dbce57e | |||
bd45cc2024 | |||
abf47deee3 | |||
ce0090f0ca | |||
6cd34614f6 | |||
264e04368b | |||
43c11c6ac5 | |||
ede42cd6aa | |||
33901dd72f | |||
ac4ae8103e | |||
2c93704b4a | |||
e2158af592 | |||
b06189ff95 | |||
d72c51c8dd | |||
e29e94a5a1 | |||
04d7eccc63 | |||
e58896bdfb | |||
d2bb4487e1 | |||
e8623fbd7d | |||
0a6de2b3ea | |||
23a8e31791 | |||
286d2b4cf6 | |||
1ab07d169d | |||
abf2d4b718 | |||
37045c77cb | |||
fd5d52a709 | |||
abcb21491c | |||
33d2b0984f | |||
fc7a040509 | |||
be09dded20 | |||
6493b9a6b3 | |||
f07fbcc196 | |||
cae90ddd08 | |||
7bd9b21e5a | |||
e29fb68375 | |||
33e0a34916 | |||
fccb2e762d | |||
956b3284d3 | |||
937d0852b3 | |||
cf2967cf34 | |||
c1e64a839d | |||
c06928a7a5 | |||
8c0cafc793 | |||
543ca43e24 | |||
31b86f539b | |||
dff1f4dd52 | |||
276cfed832 | |||
bd61c33097 | |||
e2a9c09597 | |||
5059c6295f | |||
916c1c9de2 | |||
2661700d9a | |||
02502c4ea5 | |||
382e85a8a0 | |||
05d830eb58 | |||
7b149425fa | |||
6f0e1965b6 | |||
970f1466b5 | |||
e422b1d6c1 | |||
8992f47915 | |||
fe65f4d6f8 | |||
8077e421e6 | |||
4f806a921f | |||
8efe0502f3 | |||
0eb61f34f6 | |||
cc7735e284 | |||
e24ddb9106 | |||
67d284fc35 | |||
a9d32fea37 | |||
3374481912 | |||
1972cc25a9 | |||
6a3cd0054b | |||
548dbf4b78 | |||
74af40a352 | |||
8f26c4bd07 | |||
1d51b06d3e | |||
305ee3c12e | |||
90ce89193d | |||
2ad3fa1593 | |||
4bb9949768 | |||
c4f1d2f153 | |||
ed6f82af0f | |||
3ba8cff60f | |||
7307ddd1b8 | |||
0601bedb0b | |||
0c988d95ee | |||
d593362ba8 | |||
f97f4c68ba | |||
6d60d14f4a | |||
d13bc9d420 | |||
2a41e4ce68 | |||
850654dccc | |||
2f9dfed073 | |||
57911c42d1 | |||
6064eda68f | |||
fe803a4588 | |||
2787116171 | |||
0a509cb382 | |||
b25ab941ba | |||
6ef59a5949 | |||
17fc8deb19 | |||
45b5c1c262 | |||
7fd547a2d6 | |||
d19d787f6e | |||
52474e39d9 | |||
d243ee4b4c | |||
2c2f8f5853 | |||
64ba127e7d | |||
9d22a9e664 | |||
96c55db7ca | |||
0f48563e29 | |||
7c014292d2 | |||
19507d1837 | |||
81b2dfd9d0 | |||
3f63b320c4 | |||
4da760d614 | |||
a95ac8ad83 | |||
f841d3d259 | |||
902700e1f4 | |||
bbf456a4aa | |||
134dbd2f1d | |||
d371b093d8 | |||
979c49b99a | |||
dad010a891 | |||
9f974c9401 | |||
fba3ed2244 | |||
aa1d927da6 | |||
292655cbdc | |||
a088c9ca7c | |||
8c1ec43500 | |||
1ac1c6dc9e | |||
87ffd6eef2 | |||
41139cea76 | |||
d98b7275d1 | |||
77cca1ce84 | |||
c0bbbdb7ee | |||
8e6b7aaec0 | |||
aae6820fdc | |||
2a4f35959b | |||
f71708e5c5 | |||
6de00b1f21 | |||
2b27b40142 | |||
55101b1a4a | |||
c014fd7f6d | |||
53a3be0703 | |||
04a178e7da | |||
6d5b6b2ff7 | |||
ab97de2763 | |||
34f7e4d448 | |||
f7c139030f | |||
c967308859 | |||
02207f6cfd | |||
badddcf0de | |||
b6f1c516e2 | |||
932a47a8a7 | |||
f69f78db34 | |||
fc2c465bac | |||
f2a7f8efda | |||
e6c172ac22 | |||
d8e7481118 | |||
2485ef8547 | |||
b17762f8d9 | |||
b3611eef9d | |||
a68c1adba6 | |||
42b536e40b | |||
d6e49be268 | |||
3d10dad780 | |||
c9a727594e | |||
cd25e1283a | |||
6675de1a26 | |||
96618e9517 | |||
dff239141d | |||
abbb329ba6 | |||
5bc77fc6e8 | |||
9c820fcca1 | |||
bb064a1ba7 | |||
a5d9fb518a | |||
01dd46c5ed | |||
ed51989796 | |||
bd20977ebc | |||
11e10f6eff | |||
c88265ac04 | |||
3f655ca50a | |||
359d4dc1b2 | |||
ca47446b46 | |||
cffb2db7ae | |||
0272907596 | |||
321a4b24b9 | |||
8a243ffb57 | |||
c2330fe3af | |||
9876732875 | |||
29e453c201 | |||
6c14402992 | |||
bdee525336 | |||
fbf13efe74 | |||
415df2357c | |||
4fc8800a37 | |||
6a532b836d | |||
31b94fd3ff | |||
fd733e819d | |||
7b6d52c613 | |||
df69559b39 | |||
e388f0d563 | |||
85e7b78b21 | |||
42a97f8c40 | |||
5cff247aa4 | |||
f115691365 | |||
deb66436cb | |||
b8152dd7f9 | |||
678c004a64 | |||
d9f44c1f7d | |||
088160ed32 | |||
8de004d281 | |||
bcde4337ac | |||
401210da44 | |||
beb81b657e | |||
f7b3450d65 | |||
29776c739a | |||
bdf322ceb0 | |||
fae763dbb0 | |||
c0792522a4 | |||
c67e62bac3 | |||
9dc184adff | |||
7118817df7 | |||
287b83b6c6 | |||
5d03eef051 | |||
48f7b06549 | |||
4e111cebbe | |||
fe32332e2a | |||
0bb6e1cdc2 | |||
e67bb64311 | |||
9058536406 | |||
5bc9e1632a | |||
a0a8899801 | |||
aedb4749a2 | |||
f52d49ad00 | |||
a1e7592bd8 | |||
c784d24fa6 | |||
0375c0b2c5 | |||
135e55fe27 | |||
1ed291086a | |||
f6e25627de | |||
8fe79a1fb5 | |||
a1df78517f | |||
92fa8d683a | |||
df27138401 | |||
7c7d40ea44 | |||
aa70be525d | |||
f7ac969a4a | |||
4f5e52fdd4 | |||
5183848250 | |||
69af1baf7a | |||
9a28cbc1e4 | |||
c38b457ba0 | |||
1a50f7062a | |||
8cfd80ba84 | |||
e5e14e1f9e | |||
6611464f73 | |||
51d93f0217 | |||
fdc7981d18 | |||
a63d165dd3 | |||
145a744ce0 | |||
df625a998f | |||
21c1a499ac | |||
9a81cabece | |||
b50dc206eb | |||
9044760a10 | |||
0b811773e1 | |||
627a720d4b | |||
89d45e7775 | |||
c0e6e03dc6 | |||
05fd8e2a38 | |||
dd59748bf0 | |||
38ceaf5253 | |||
b2fba5083b | |||
be6a209fe9 | |||
0f0305c602 | |||
d5350fd719 | |||
ea75f63dfb | |||
de8e530b37 | |||
bff927c6eb | |||
985bb44559 | |||
9c44cae5b8 | |||
6872455922 | |||
ce13a5152b | |||
d7c13cc291 | |||
022c0746c0 | |||
06c3f57f62 | |||
9da27cc56e | |||
92c5497eab | |||
f115fe47fe | |||
224f08279b | |||
a3b660a2c9 | |||
89df50da4e | |||
cce3b3a559 | |||
f53cc10338 | |||
61fb4d584c | |||
b93e2ca4cf | |||
325b3c6271 | |||
859e9ca653 | |||
441e2a69d8 | |||
af937f2e31 | |||
912629c2dc | |||
879fa484f6 | |||
e86103fdcc | |||
920ffa8c24 | |||
488f81ef74 | |||
0a5461cbea | |||
40c934c544 | |||
bb43e2aa03 | |||
ed277357cf | |||
bf1fb8b7bd | |||
e615cba9a5 | |||
8adabe1f74 | |||
896b34d1a2 | |||
5fd1865504 | |||
0b318a19c6 | |||
622d6c0cab | |||
3f4140900d | |||
b434bc93a3 | |||
11529f2795 | |||
d86b030796 | |||
a7e4657752 | |||
2f28fcba05 | |||
6da350aee6 | |||
9585f9a1a6 | |||
fd4876be24 | |||
31e2fe6a4c | |||
aa51968603 | |||
a3336368e5 | |||
33c0c6ff3b | |||
eaf37c828b | |||
d65a8e84f0 | |||
b000d96dd6 | |||
cd867f800e | |||
124f0e7093 | |||
fb897e37d1 | |||
3e5a48e5e4 | |||
3f88a67865 | |||
ef0b546d4c | |||
088f8b8b54 | |||
1ebcafb25d | |||
90396153f4 | |||
0fa93cf615 | |||
15a7a2b0ea | |||
446c254bc8 | |||
e41edc1fb7 | |||
738f776d36 | |||
e77db309b8 | |||
a6c1de1cb2 | |||
ace54f8175 | |||
d7043bcaeb | |||
04bbc764a0 | |||
91f7056767 | |||
8299093bf8 | |||
a0dffcf51f | |||
b3daf7d760 | |||
724e1d33b6 | |||
9deaff9181 | |||
b9ea6e8d96 | |||
231771e16c | |||
5b8308b3d2 | |||
f91f9c4862 | |||
24b848faac | |||
7d0ea614da | |||
2615b067e2 | |||
cd1abb60d7 | |||
436ec0ced7 | |||
937fb85376 | |||
a405324907 | |||
fba55711f2 | |||
208552f0b4 | |||
c7cdb950ce | |||
921169b3ad | |||
d7d3731567 | |||
d5ff5fd6f8 | |||
e195257d2a | |||
466ec7b962 | |||
8bfe59c8a8 | |||
de512216c4 | |||
9936b402a6 | |||
bd2dfaad2e | |||
8e539bebea | |||
022cde2c00 | |||
572f58a3a4 | |||
07e2bdac81 | |||
fb00929ee9 | |||
fb5da15746 | |||
48363aa3b0 | |||
090a7794b5 | |||
c63d8e7a30 | |||
d6ea69a115 | |||
4358f9fd2a | |||
af2ef36d68 | |||
316211372c | |||
29a2d41331 | |||
9f8046324d | |||
af05c34da3 | |||
a4410f3a02 | |||
4503199d25 | |||
c275adbb91 | |||
4061232fe3 | |||
507471e318 | |||
f0f613e2cf | |||
0ec8def0d8 | |||
fbaaed1516 | |||
152c196c65 | |||
e4dc84a5d8 | |||
5fb3b0e0e3 | |||
2854fb5f6c | |||
a9adb2f1a2 | |||
51383afd50 | |||
10bdde4459 | |||
4b02ecd6e8 | |||
b84638ac5a | |||
79f2882aaf | |||
1c978f64b1 | |||
5e9496ef36 | |||
8b6268966e | |||
52434819c3 | |||
29eb87b7ef | |||
c38026886a | |||
63e330b83d | |||
164da0fd9f | |||
e30b1de100 | |||
6940ad3fd9 | |||
8ae15141f6 | |||
89007923c7 | |||
4c10b9844b | |||
f3d69599aa | |||
fc8f91baec | |||
853bf3065a | |||
8d712c81d4 | |||
aa0597da2a | |||
a29f33020d | |||
9eb441ac44 | |||
10e7c96379 | |||
6c474daacd | |||
bb7ed73743 | |||
ea749d69a3 | |||
5bc0dfd616 | |||
fef34dfe82 | |||
a3dd5c1e92 | |||
d873d653d0 | |||
d9934ad8db | |||
26e5364aea | |||
91451111a2 | |||
b104bec49d | |||
0ac33b64b1 | |||
82141cec6e | |||
218313428f | |||
44b47b49bc | |||
2f69317f5d | |||
e1eff7b744 | |||
3aa12281c3 | |||
72920130c0 | |||
0fd00331e1 | |||
7a4763ee68 | |||
647a78b791 | |||
82faa91ce3 | |||
ad664dfb9f | |||
005ac9e732 | |||
d8e3fe542d | |||
c40e4f6c5a | |||
9bb3195bca | |||
eee0bc4985 | |||
a24d670f54 | |||
51e049ab78 | |||
01e37dfab8 | |||
74087edebb | |||
db58c9b77f | |||
c4dad1c20b | |||
cae04656b9 | |||
6586c4e387 | |||
4e60a81b36 | |||
464b4b18a4 | |||
e5c0969047 | |||
ab0d144300 | |||
ac3823e10a | |||
d3a4126e27 | |||
3a62acc54d | |||
bf8268adc4 | |||
7d4f25b354 | |||
0f2d480036 | |||
043e3784e8 | |||
58ab06b4f8 | |||
11544fe8ef | |||
8776a45ee9 | |||
a7bb059096 | |||
ba8977b47f | |||
032a6adaab | |||
460fbb18c7 | |||
978ac50015 | |||
b323b9c843 | |||
1afcf34829 | |||
48d9ad00e1 | |||
ca10356fd9 | |||
275bd44e15 | |||
beb2d880ea | |||
61d2107e9c | |||
b06f1c81bc | |||
8bb83782c7 | |||
aa05458f1d | |||
c694160c9c | |||
5b24a8f21d | |||
2c23c42c98 | |||
edcadb7dd1 | |||
3bce3502d2 | |||
9942227c6c | |||
02b5c3da71 | |||
ef533b4c87 | |||
3ecc883944 | |||
a1fadce7c6 | |||
79bc1290ae | |||
10272ef395 | |||
f03c49850b | |||
6677c10173 | |||
3223a3ac54 | |||
497fe1e68a | |||
3b334c4230 | |||
7f115f2e83 | |||
12aa04be93 | |||
a7ece4fdf3 | |||
57d1ed1073 | |||
b04ebb1782 | |||
958dbfdfa5 | |||
eb724336f5 | |||
9cdd4bee97 | |||
74cc77400c | |||
247a39c0a9 | |||
1b0c13a417 | |||
abb2e231f6 | |||
50ef4cc5da | |||
34bf9b729e | |||
0a6f607e22 | |||
9e4c61c139 | |||
144418434b | |||
a20ad68fe3 | |||
a50a3df716 | |||
f29124773b | |||
c1235897df | |||
4a52869d23 | |||
f9098b5379 | |||
6a95ff56df | |||
340829bb71 | |||
8aa48effaa | |||
a5fadcda15 | |||
f515674dff | |||
72ef5d8f8d | |||
a0a077eaaa | |||
8feb4365dd | |||
94b2dd74de | |||
678597cc96 | |||
417d82de58 | |||
f3adff1da1 | |||
0473c36c6f | |||
c9cb75aee1 | |||
36011e124a | |||
b9420040f5 | |||
59b925a028 | |||
7af075633b | |||
09891bb0ad | |||
160ebe01d9 | |||
a096e6b337 | |||
3c41a5e910 | |||
a3e39987d4 | |||
47f5ea881f | |||
faf8f1fbbc | |||
9f9de27a57 | |||
9a3ec56eb4 | |||
4f03ee814a | |||
6f84815801 | |||
9588a582ce | |||
ed8e549ecb | |||
c79ebd4eeb | |||
2a85bb28b9 | |||
13ea1342fb | |||
c707d4bfd8 | |||
aeacdad484 | |||
6d9bec3e0b | |||
1b5554eeda | |||
bf140be75e | |||
eb4c7c6841 | |||
dcd3c709fe | |||
f966187ea4 | |||
a288c0f280 | |||
a746d4cc3a | |||
f1cca207fc | |||
f4bb9b604a | |||
7be6ee9a68 | |||
a22c79c58a | |||
b642e019e8 | |||
578bab5fdd | |||
b48f08e65c | |||
81c14ba610 | |||
4b84fb5ac5 | |||
7f5e650796 | |||
c22e2e8159 | |||
ee8a53188c | |||
98f86a44ef | |||
1b3169e0d0 | |||
8273a396c8 | |||
5bad914411 | |||
879d260202 | |||
ce4d75f62a | |||
f30622424a | |||
09e7d56ff2 | |||
5ca23b5363 | |||
f7e70d25ea | |||
4338c41112 | |||
01f9b25be2 | |||
8b0458cdf6 | |||
bed978a26a | |||
73fbc81067 | |||
0d7f84857c | |||
42d6815ece | |||
d1db9fb659 | |||
b74fb2ef5e | |||
f3e228e8a4 | |||
6b5742c1ff | |||
57595988f5 | |||
81418a7712 | |||
885c7bbb10 | |||
d4c25359bd | |||
44f406b4b9 | |||
427d2fed8c | |||
51d454cded | |||
ab2bdfc508 | |||
3892b93bca | |||
83c2e907c7 | |||
32b7cc68b9 | |||
a61e3cd689 | |||
f922598127 | |||
e11c289150 | |||
1b37c61b5a | |||
2d3bfa9a89 | |||
e414b9edf1 | |||
62d3fc65e0 | |||
262ad45b79 | |||
cd90702fe5 | |||
012b1b56aa | |||
ff999a6dda | |||
797553ce16 | |||
8f82c8ad3d | |||
7a2c132b8e | |||
ba9f6fef99 | |||
6633a96245 | |||
745f8d32b5 | |||
f715478070 | |||
5f2aaeac57 | |||
044a4f7575 | |||
83d5e458ca | |||
f7669b6797 | |||
eb56567812 | |||
489f981e40 | |||
19adbeebd5 | |||
dc93368e03 | |||
8d3166c5fe | |||
07caea8b4e | |||
141b7ac554 | |||
4bc5f1401f | |||
13a2d3dfdd | |||
d62f0de862 | |||
0073ddf237 | |||
e411f54236 | |||
6025b44e5b | |||
f4f427dd2a | |||
ea226a1697 | |||
26c5c9c839 | |||
5cddb269d6 | |||
0d5099f230 | |||
b55814a1c0 | |||
eb5382e0de | |||
39d509a756 | |||
b3f1714ba9 | |||
708525ef9d | |||
df14e6d43e | |||
faedd325be | |||
600fbb2ef8 | |||
b4aedb5f84 | |||
ed7ebf2da1 | |||
dd1e6402c9 | |||
78689e7443 | |||
aa57b1bc77 | |||
491d476cac | |||
f0053a2f78 | |||
10e7a3b35b | |||
4147fd6b19 | |||
2bb903088e | |||
c90f985fcc | |||
2ebaacfc89 | |||
c339bd49d0 | |||
c349fb0e37 | |||
bc825bdefa | |||
ed49ce8e1d | |||
ad2ecd538d | |||
ff8e3f0af4 | |||
698e17178a | |||
ebeee70931 | |||
b8b118bdeb | |||
5ddd7d1b14 | |||
450b23436f | |||
89793ac338 | |||
c456812a46 | |||
6f8f6e9233 | |||
5770d00f81 | |||
a0fb1eff4a | |||
89dc240a22 | |||
ee4f069341 | |||
011bb9f5b1 | |||
08b06e1b4e | |||
0416a2ff15 | |||
3a7cdfcaa4 | |||
c36a47576d | |||
f14af7cf83 | |||
4014c48c62 | |||
6c9135c093 | |||
19993199db | |||
5b9f362925 | |||
80ea9001b3 | |||
24bb94ceac | |||
0f16351f5f | |||
b60b26bbd0 | |||
86e53e08de | |||
d3cb10a74e | |||
7d6cfff719 | |||
cc0fe0a1a9 | |||
25327342fb | |||
e02cf67f85 | |||
bf4bef6d62 | |||
76645bce6e | |||
9276c491bc | |||
fa59b4f8a7 | |||
934a37c36b | |||
5362f62078 | |||
ccd360687e | |||
5a2e8a838c | |||
3abae1cc75 | |||
b68ef8c983 | |||
d5f5ba95bb | |||
e8638cb0b3 | |||
62f9071adc | |||
1d079dd9a4 | |||
cccb56bda1 | |||
5d8dc241d8 | |||
9ba7312caf | |||
8ebda219c4 | |||
47f14e8555 | |||
974a24d03b | |||
15f225537e | |||
a32572fc96 | |||
be3ed9b6af | |||
a0939e1c48 | |||
003dca9d45 | |||
5c1770247c | |||
021dde66eb | |||
5840a3e1e2 | |||
7c6478fe6b | |||
68aca55e6f | |||
ba674935f4 | |||
a053d55fbc | |||
38ba8852a3 | |||
3533359fae | |||
0a988d1c69 | |||
5f9e65cc9b | |||
026188268d | |||
0e3464457c | |||
56195434e7 | |||
ba2194f435 | |||
e7df172da1 | |||
e7606e6dca | |||
8d4c0f505c | |||
8f2878a841 | |||
77296348a0 | |||
a62a7d5330 | |||
bf60aae9d8 | |||
ecc1520100 | |||
f1f6a2b341 | |||
55bf1c31a6 | |||
e47dd3d587 | |||
af0e3a278f | |||
493ad93957 | |||
dbe8f3cfbe | |||
08cdac968d | |||
f12d5ba689 | |||
0afd77d110 | |||
7551941ef2 | |||
9ca0307e1c | |||
9a6f8be28c | |||
9baf3b5a09 | |||
ca3f0873f3 | |||
adb0201449 | |||
cf293642fb | |||
10e1106760 | |||
3f2d375a53 | |||
f8e121ee06 | |||
0ee005579b | |||
6ecd7fced8 | |||
aeaf4d78f8 | |||
7baf0ddcc2 | |||
d79e141fe5 | |||
030071e659 | |||
9cbf226cfd | |||
36aabf23e1 | |||
8b67255186 | |||
3186661420 | |||
46896d9e86 | |||
2c4fd340c8 | |||
ae6d052978 | |||
974891a085 | |||
d44cd16682 | |||
23e99a3ed8 | |||
024a457250 | |||
788cb843fc | |||
790e0908a3 | |||
7a45cd5b56 | |||
f61a8ce51d | |||
fcce29a467 | |||
5f568733f3 | |||
96340de17d | |||
3611f67fb4 | |||
353ccbd444 | |||
3c1179d27b | |||
e502caee9f | |||
da8b870670 | |||
6b26859983 | |||
7afd224aff | |||
62e7bead73 | |||
116f7d1c4a | |||
18f89cc341 | |||
7c99ae1b3b | |||
16dc4d298d | |||
762c378bd6 | |||
515289134e | |||
3d1afe7cf2 | |||
fd825b1049 | |||
136e90638a | |||
9bf071132d | |||
014bb2f426 | |||
56927927c8 | |||
b19a4d2977 | |||
f4b838d8e2 | |||
c6cfd24f19 | |||
10f36f40d6 | |||
9d5cf9163a | |||
9abce0cca3 | |||
c6245f4fa3 | |||
75fc160204 | |||
263198dd89 | |||
345f96055d | |||
51144aa45e | |||
86a599d13f | |||
0cf81e6f7a | |||
8874fe973c | |||
f8a03226ee | |||
32db1e3045 | |||
303e6c0102 | |||
18883f1ba3 | |||
5c31271e91 | |||
00981cf4e8 | |||
968f4a69e8 | |||
e7e1a9bf50 | |||
fe1becb001 | |||
7789171c71 | |||
3fd2222c99 | |||
6de36a88c0 | |||
b37685542d | |||
a2b1b9e746 | |||
8017324033 | |||
7464497c88 | |||
499def3daa | |||
6931b75cc5 | |||
f853610578 | |||
69f51b88bf | |||
e0d680201a | |||
1566b8f8b8 | |||
4bbf78e840 | |||
7ab16a69df | |||
95e60ed775 | |||
d38cd2547a | |||
2159b72e69 | |||
81c23bbf9d | |||
0d5b8edf31 | |||
fcdb80830b | |||
50b48ab25c | |||
31b45666b0 | |||
233e76724a | |||
af637a82c3 | |||
ea32ea11f2 | |||
1b7a0de745 | |||
50e0cb65d9 | |||
ba4807f62c | |||
5efc02a238 | |||
8e50ac67bc | |||
a3c03e8ceb | |||
5a3e30b30a | |||
e3ab90042d | |||
f35c15f7d2 | |||
32387cd034 | |||
cf5c816483 | |||
bf9b9ca54c | |||
0ca2ca33c2 | |||
51f25e96e9 | |||
1875047638 | |||
fa4d61eaf0 | |||
49eb638e15 | |||
fc1f290b85 | |||
9194dc0161 | |||
0d480dbf7c | |||
183e83684a | |||
7b4ac7998a | |||
d75c6b0c36 | |||
40b222f8bc | |||
aa7dfb7bee | |||
6c1453eb54 | |||
c1845aec83 | |||
eb8479ac9a | |||
636c027298 | |||
02e187f066 | |||
854112095b | |||
a71c805959 | |||
c3ced0d089 | |||
80996ea63e | |||
aff51f8af1 | |||
ccbb81e9f5 | |||
f88dd28c51 | |||
a65a71df5d | |||
22f2ecc433 | |||
7f90ad7847 | |||
1292c0ecea | |||
55b7d5025b | |||
6a310bbaa9 | |||
bc8753da85 | |||
7f63e318f1 | |||
6c749319cf | |||
7a4463e104 | |||
e1be4ba925 | |||
34d21c1de3 | |||
fae36aebf4 | |||
75e828923a | |||
b499b87f8c | |||
233dbec4b3 | |||
d56ff9592e | |||
08f6317beb | |||
a75457ad88 | |||
b0482003bd | |||
634356e72f | |||
6d3cc16ab1 | |||
6027671c09 | |||
29d0cb4a15 | |||
fe7001975a | |||
ac88f1c146 | |||
b5b86218c5 | |||
bdcc6e52e6 | |||
0eae817aa6 | |||
8994b42760 | |||
6a63ce992a | |||
9ae6285eef | |||
8f9737f567 | |||
f287d313c3 | |||
e745836404 | |||
08baf798aa | |||
8bcb14c65d | |||
d94dc68830 | |||
297fed6aef | |||
d690d6e0e3 | |||
9ba8d88b07 | |||
34a40b0131 | |||
182bf5f2bd | |||
04638535d8 | |||
d87c8428fe | |||
166fb9a8e4 | |||
28a21d0b8f | |||
d1d1d60c30 | |||
80fd49d60b | |||
34eb1331a3 | |||
bff329a329 | |||
604929d002 | |||
4a9151e4aa | |||
020cc89576 | |||
25898d34ca | |||
6394388714 | |||
d4101c7bdf | |||
c93bf89cbe | |||
88d1f29fe2 | |||
c437a33f2a | |||
e3259f39f1 | |||
cb357b0a16 | |||
a7faf445c4 | |||
82a08f24c0 | |||
afa89ac125 | |||
2060b5cd34 | |||
d69730a333 | |||
9714a30148 | |||
23c0f2c313 | |||
5c4139be45 | |||
6b1a3a20e5 | |||
4ae00c80ca | |||
827792c4f0 | |||
abbe700dac | |||
1347bfe243 | |||
a76ee95b6d | |||
f3689f09cd | |||
d545cfd38c | |||
3631a9fac2 | |||
aee4ad2d3f | |||
f88c86c799 | |||
60ac27e401 | |||
d0567de4e6 | |||
ca30fd6088 | |||
1470e9d5ca | |||
f45efe2aa8 | |||
5b6c475817 | |||
4abd2d709f | |||
d97aff85b3 | |||
deec65446f | |||
5c662b1ae1 | |||
f648940388 | |||
5aae17754f | |||
0ef0f6ece1 | |||
bfd46f28e0 | |||
eaece18afc | |||
67d39b037c | |||
dd3f5a146d | |||
9fdc5b4b9d | |||
1f32d13698 | |||
886b1019ed | |||
3e8ed8a171 | |||
8307daee63 | |||
9b40d10352 | |||
f2a06eab37 | |||
74fd70416f | |||
b85c164195 | |||
75c41b645a | |||
54c8b3ef29 | |||
56bde40035 | |||
ff4a015baa | |||
0db4fcc27e | |||
f3080b6277 | |||
69cbbd5811 | |||
0b85760939 | |||
03f3a4805f | |||
d95adf2631 | |||
e971d40e06 | |||
c65a01a5f0 | |||
8586014e17 | |||
bdfae4ba04 | |||
75cb94b51a | |||
2f6d163a7a | |||
ecfe72bcad | |||
e6ff9e18cc | |||
3c550c1781 | |||
537693f5cf | |||
5ae0589547 | |||
71fc6fc257 | |||
c0d7b16ee6 | |||
f3f7aa9e1d | |||
43355970db | |||
bfa386acba | |||
e8b432485d | |||
a12a34e3bb | |||
b79855c01d | |||
17fe501a6d | |||
8201b367ec | |||
6c242084ca | |||
aefe7b176a | |||
6059b85e58 | |||
aa46c52eee | |||
d3cbfbdb59 | |||
cc9b77b876 | |||
1568ac9e8a | |||
1129dacdfa | |||
fab7967018 | |||
bb40a4d6b8 | |||
90d27147e6 | |||
634247c590 | |||
fd8f7ea693 | |||
5eeb497f2b | |||
505e642691 | |||
74a7e2a17e | |||
5fec956ce6 | |||
1794782323 | |||
0210ee8747 | |||
ca412832ef | |||
1089c25b8f | |||
e85841784c | |||
ca2236958a | |||
c7686323b7 | |||
73d1a1a05e | |||
211f7b591b | |||
72ea256906 | |||
f521622d4d | |||
dc5283ce9a | |||
256a4197c9 | |||
a5a12f8b3a | |||
bbe180ecd1 | |||
67678cd49e | |||
097d4fe34c | |||
5914346ace | |||
062788f222 | |||
55be9b9ca5 | |||
0da2f91771 | |||
ff190e02d4 | |||
29fd5747eb | |||
fa8f5bc0d8 | |||
2118434823 | |||
2eeac0bf8b | |||
89b293fecd | |||
3f758d5981 | |||
e838bb43d2 | |||
b7b83305b2 | |||
8df3080e0d | |||
f88794c752 | |||
cc9e2cee1f | |||
91cb892c74 | |||
a26f908370 | |||
4d14f56fa8 | |||
d9a2255be9 | |||
5e3d71c6c5 | |||
619d94bf36 | |||
6069659e0f | |||
f6a79bde6f | |||
bb9e230b35 | |||
bc9417e16b | |||
a4313d388d | |||
4ebb3a894d | |||
0642889b64 | |||
3094d084d6 | |||
f9fec74ffd | |||
8ef3ab0d49 | |||
e9a6f8ef46 | |||
68724752f8 | |||
de8fa09366 | |||
e619870eec | |||
4be5f0dab3 | |||
abe1929b49 | |||
68c4116327 | |||
3be9881997 | |||
2e44f29882 | |||
a5520c1936 | |||
112cdd54e3 | |||
b512c67b5d | |||
d8fa7bc9d2 | |||
41397ab41d | |||
c437f1473c | |||
6020cd011d | |||
582bb3e2ca | |||
5c67161dce | |||
c00eaae62b | |||
8e4dd030d0 | |||
4d7b188999 | |||
6de260d73f | |||
2230ad59f1 | |||
e22b1661f4 | |||
bb723076ee | |||
fd9c24413d | |||
91c58640a7 | |||
c8e3375248 | |||
aeef8c02d8 | |||
08f2cd2472 | |||
fe413d52d6 | |||
add2ca0b8f | |||
ad6cdc9017 | |||
dd8cab4562 | |||
f7c791d153 | |||
b66f06d9dc | |||
89940677cc | |||
56e7a1e2a0 | |||
810f5ad531 | |||
495f5d03ac | |||
f63d2cebfc | |||
ae60e8cbd5 | |||
772bef05fc | |||
9320d1f7a4 | |||
e8912c5dc9 | |||
2b73a9d2a4 | |||
e25ce768bb | |||
05c628b33c | |||
fda0aa3ce1 | |||
cdf5bbadea | |||
c6b89a826c | |||
6264e56148 | |||
7f8bfd759f | |||
c3c2013944 | |||
1e4a01399d | |||
03967b67cf | |||
7425478a55 | |||
f807447de1 | |||
706163e7a6 | |||
a4c145c1ef | |||
c445ea90ba | |||
52c50398b8 | |||
fb89f77db7 | |||
f7b94179a4 | |||
e045ca8538 | |||
871e17c2f5 | |||
f86c3c81bf | |||
71ab6d38e4 | |||
e76fb7a524 | |||
90a99dde1f | |||
7b9f5d0e9f | |||
e4d4dbbeb6 | |||
6ef94fb59b | |||
a03dceff7d | |||
f717c57648 | |||
ca8fdad422 | |||
faa61923fb | |||
96a39f5c54 | |||
72f8c4d5e2 | |||
07cae4d684 | |||
dd56d7c0bb | |||
77d986f213 | |||
13bcefe5cd | |||
49d0e06704 | |||
2c8790c545 | |||
d0260acd3d | |||
44ec6184c8 | |||
cfa9729831 | |||
c25af3d5ad | |||
d3e9200a7f | |||
2032ba3ba3 | |||
9bcde69ee0 | |||
beca2b429c | |||
3a1699f0b3 | |||
a7192e866f | |||
dc882b4dce | |||
77b4de3941 | |||
006d17aac7 | |||
1a3a1db4ff | |||
97fa659283 | |||
f1d84ccb49 | |||
1f14240251 | |||
ea6fed6ecf | |||
d09eca7833 | |||
2c6f64c5ae | |||
ec87e4359b | |||
2b63bae989 | |||
82f4e3157a | |||
725ceab00b | |||
ba428c401d | |||
510669ee2c | |||
8d749df290 | |||
091c0c0c71 | |||
7fdd2cacd7 | |||
2241a0b2de | |||
d21a93123b | |||
e542a8d8e2 | |||
94ee4e7fb5 | |||
e3d430eb5e | |||
fd76255cf6 | |||
d180631877 | |||
1977e21363 | |||
e1a3ee1b81 | |||
cc43d9daed | |||
79705df499 | |||
36bbb906c1 | |||
816cc17ed3 | |||
97e3b5d2ab | |||
79ab9d80f2 | |||
32511149d1 | |||
cc9fd53abb | |||
4061c7450b | |||
9ad535bde6 | |||
b067096fc7 | |||
2dd58e5f7d | |||
7c42ab885b | |||
26b283d44d | |||
8c1b07c4ba | |||
f98e0858a7 | |||
8b60d5bfcb | |||
30b4c6e755 | |||
3d2a98451b | |||
aba528b227 | |||
d971768056 | |||
2e39be6625 | |||
f514d466a6 | |||
d10bf45283 | |||
a0064a1699 | |||
907472403d | |||
a9b6db9ee9 | |||
3e1dc9f400 | |||
d30c019b89 | |||
86b8712dd1 | |||
44241e03da | |||
12dcc2c31f | |||
bb89b72a81 | |||
ea790faeb3 | |||
4ef7b16925 | |||
93e244b4c4 | |||
87281d34c1 | |||
20041701cd | |||
f9c5379400 | |||
2a531f1a1e | |||
4d4b9c0d6d | |||
dc592e92b5 | |||
0db1a3167d | |||
830f792824 | |||
a13ebc3975 | |||
b28ef61618 | |||
6f297161de | |||
59c626b4a8 | |||
1d014a5a94 | |||
2dc8159d96 | |||
453f742732 | |||
5e6cf9fb02 | |||
83349fc72d | |||
979a5c8c16 | |||
9f625835ec | |||
5fd379e71b | |||
9c5b497751 | |||
4dc5f3e7d9 | |||
13954ffe01 | |||
36d4e1f7ef | |||
b716a2f8ac | |||
f98095e6cb | |||
d183aca810 | |||
52f4bddbce | |||
b837424f29 | |||
ba2a8c82f8 | |||
2856d9d6a3 | |||
71fac76e3d | |||
125f1ae34c | |||
b418169c20 | |||
f4d12ba622 | |||
c64d8c8b6b | |||
10a1ba95d6 | |||
27d3daf918 | |||
dcbd72e64d | |||
52e1e93f9d | |||
7d3d0999f3 | |||
93f90b5a62 | |||
c2b113ac0a | |||
8ff8ab4f27 | |||
414b8c9f21 | |||
4975787afa | |||
1210691fdd | |||
2a4527a8d6 | |||
2991906a85 | |||
5b1f4f189b | |||
d77a1e6925 | |||
19c713ebb2 | |||
90e0e0b72a | |||
22bbcaeed0 | |||
d7b8015df7 | |||
c1ac47e1ce | |||
e375101132 | |||
05b14bae7b | |||
eb15fe3898 | |||
4f5518bdd8 | |||
c9e1e6e020 | |||
ade73e6892 | |||
ee2aae7e3a | |||
b6011d4cf5 | |||
a31c6ff875 | |||
69baaac27e | |||
b16a90e9d9 | |||
f31aa622c0 | |||
4578edf157 | |||
33df35db1b | |||
093ddd776b | |||
da10b27219 | |||
5b4ed6f926 | |||
8fc467652d | |||
7971b64d57 | |||
9c1e2c3c45 | |||
909917e133 | |||
3b6c37a30b | |||
4a6e2a5d99 | |||
6cf84256fe | |||
876831480a | |||
aebc9a3b9e | |||
7b28614c37 | |||
4524c705da | |||
1f70be688a | |||
500eedaab7 | |||
2d2ff0a29d | |||
6d0689fe6c | |||
0b3dda18d3 | |||
09a8a494a0 | |||
11ac4df5d7 | |||
d352405ba6 | |||
a81609fd2c | |||
bf05952582 | |||
596a24fce8 | |||
9f20e40257 | |||
8be67a4431 | |||
58a2f7a874 | |||
cb92143613 | |||
08e26aa30d | |||
8e3ffe87b8 | |||
20e2bf9682 | |||
8512f97386 | |||
3ce880bc62 | |||
72ae243fa2 | |||
91829b0e7d | |||
7c3cd10696 | |||
24bdee626f | |||
6a30a75e3e | |||
ccdc336112 | |||
a4b71f4d11 | |||
c3f61e86b7 | |||
d8d93ee344 | |||
8ffff44454 | |||
568b90d0b4 | |||
46e09d174b | |||
1698a85e99 | |||
c9b62209c2 | |||
b280d6a76b | |||
29993e6412 | |||
2a5edf4547 | |||
d58c517a6c | |||
50136c319f | |||
2fb3b50535 | |||
4171e87b4b | |||
60b3036037 | |||
dfb2487640 | |||
97454ca162 | |||
4200409f79 | |||
b6a06189fb | |||
be521804c8 | |||
e95fcf6172 | |||
fbd2235a51 | |||
31b1b83606 | |||
a5d4f63281 | |||
328f9a70d3 | |||
df2b1dbeb1 | |||
f768393a4b | |||
c0a0d60c87 | |||
9cf5a4cac0 | |||
f21a030cf8 | |||
74e3d387eb | |||
8f83f497d5 | |||
6999fa858e | |||
8c1bedf796 | |||
1090c04fe3 | |||
33b04427d5 | |||
f7bb356abd | |||
e16bf0698e | |||
e6190683dd | |||
e08e41ae0d | |||
5f1a89df63 | |||
f15df40a54 | |||
a32e0e4ec5 | |||
3e8ac6b2d0 | |||
50a773f456 | |||
42484d718a | |||
81887000a8 | |||
987473df44 | |||
3680eb0bf5 | |||
3dbdc495e7 | |||
466515c801 | |||
e198f7e671 | |||
5fe1799dab | |||
ce7118084a | |||
06786322ca | |||
130b7501d1 | |||
864f001c3e | |||
1553ce973f | |||
72811e59f5 | |||
4c1da3575b | |||
05c0516a57 | |||
fe6dff9086 | |||
b6df5e6ee6 | |||
3ee5774870 | |||
c8fbb96f49 | |||
143303f7df | |||
585f7ec17d | |||
9beeca652f | |||
cd92569355 | |||
a82e1d0e45 | |||
5ad06df4ac | |||
5cfd5da338 | |||
b1d7167112 | |||
a475ecec4d | |||
5c98e020f4 | |||
eed295587d | |||
237af4b07d | |||
658860fdff | |||
21ba371a32 | |||
589160242e | |||
4de8b6e9a8 | |||
e79d536f33 | |||
9e90096328 | |||
f0a382c21a | |||
682a2c7546 | |||
2d1e85f280 | |||
dbec4fc15e | |||
95cd77e749 | |||
1f8126e2af | |||
86db7497e9 | |||
172305fc6a | |||
dad9dcd742 | |||
59b90a94d0 | |||
93fc5944f3 | |||
7039216eae | |||
7ba898f701 | |||
11262f86f9 | |||
d1db2d60ec | |||
156e43290e | |||
6687c80b2d | |||
1fbec7bf3d | |||
6196480d1d | |||
2723aeeb5c | |||
728ab18017 | |||
0714fdc7e6 | |||
7bfdfee27b | |||
10ec3a9b0c | |||
eec728f162 | |||
1ac8ef5341 | |||
18cdddf433 | |||
02a697031f | |||
6beff242b0 | |||
46cc078e93 | |||
9aa6da0642 | |||
8bd20c39aa | |||
efd36388b0 | |||
79bb207a8d | |||
0fe350af9a | |||
4e784cd7c3 | |||
12d6919421 | |||
79ec4faddb | |||
6603c0b990 | |||
b5dbdd1774 | |||
a08cea9df8 | |||
5d9c817461 | |||
f95c9a12c9 | |||
015257fe75 | |||
dd5692bb2d | |||
bc3d5dc863 | |||
a4b6003e58 | |||
8086d1db46 | |||
26f4f53ec2 | |||
6af78418a4 | |||
f629db3c10 | |||
af0cf9e52d | |||
e885469504 | |||
ca7e5260f0 | |||
dba64f849b | |||
02e43bafd6 | |||
637dda2e22 | |||
575eaee1d2 | |||
48a1e8f74c | |||
4d65038ad3 | |||
6e8a41f898 | |||
8da11dbdb9 | |||
70fabf6a6b | |||
43fafbc747 | |||
88e64c878b | |||
0ad9e4af0b | |||
a6df745daa | |||
c64bd81339 | |||
c31f0b4bb3 | |||
136136d055 | |||
c20d86e5c0 | |||
7ca99f749b | |||
7cc4405c09 | |||
62d5deaa6f | |||
7f5879ed6f | |||
a0f7761a37 | |||
ed77c60283 | |||
8f144316a6 | |||
e73eed4a9b | |||
9de3da33aa | |||
0de214c3b5 | |||
1226023dc2 | |||
5e24054a0b | |||
af2b886599 | |||
1d1e5f1f99 | |||
49628e9cf5 | |||
47bc1f7a9f | |||
f9783407bd | |||
74ffa14304 | |||
e881488bcc | |||
97ee7b81af | |||
ff6eefe1c4 | |||
9f546d13c2 | |||
2e6fc70353 | |||
270cacb1d7 | |||
e2ecf0ce5f | |||
5d396bfb7c | |||
de6cc8394e | |||
4b7159648a | |||
eb9c5f95db | |||
55e9d2880c | |||
ec9c19ce7d | |||
31731e8f26 | |||
bfb12bc7c1 | |||
4befcf3819 | |||
cb58145361 | |||
b83efd90a8 | |||
9f0da3f1d6 | |||
50ae08ed8d | |||
5385642a5b | |||
0a27d4e185 | |||
bd8b9febd2 | |||
a30705f197 | |||
877032a757 | |||
19bf47b6d2 | |||
a9bfeb058b | |||
9213fc6999 | |||
447dfd1e3c | |||
638d3a32cf | |||
17c59657c3 | |||
81bce8ef76 | |||
78314077bb | |||
a7840bc247 | |||
6d0254c5e5 | |||
06681a3db7 | |||
55de2b7d97 | |||
065ada3d17 | |||
0ee2bf5254 | |||
0fe0088ff0 | |||
492a24ec17 | |||
17a6ea973e | |||
deaba48431 | |||
eb662f1234 | |||
5ecdecea98 | |||
b4277faf90 | |||
09902566ad | |||
dc80a5ffbd | |||
b1b97c19d4 | |||
46a0820e5c | |||
6cbdbb5be3 | |||
e753539c6d | |||
b8d1a88623 | |||
b84635ffec | |||
ed2fd00603 | |||
af20c613a4 | |||
b27669ee32 | |||
81d39ea272 | |||
840437580f | |||
936ede9aba | |||
2fa5d0cbaf | |||
e28f69cddf | |||
11f6c44442 | |||
8c9db2db61 | |||
7e7d27505a | |||
b4211ddc0c | |||
ff906e8ee7 | |||
c1ebccd0f4 | |||
9c223b48a3 | |||
16037dd9bd | |||
742e8c9f50 | |||
194cdf3b5f | |||
5fbeeade94 | |||
72f029b57f | |||
67c4781376 | |||
fe49286d97 | |||
4196a0f585 | |||
a6a9b13545 | |||
fa8d0946e9 | |||
1844b8c5a2 | |||
7c503648ff | |||
a598ebf72f | |||
d8ac35d259 | |||
5029e4a28c | |||
908e60dea4 | |||
d577bf300c | |||
579a606f93 | |||
ac15c0c57e | |||
0c0372dc51 | |||
723c0e99a5 | |||
e04596c668 | |||
a809b05808 | |||
da44dc3fb5 | |||
06c63f1207 | |||
c3425346b7 | |||
fe8e4a4f54 | |||
abab778e2e | |||
03ecb2fe13 | |||
a78f89d4eb | |||
499c2213ee | |||
abbc7b572a | |||
bf1fdda651 | |||
218ea7267d | |||
70cf085df9 | |||
71783657af | |||
137c21e6c9 | |||
bc473055b9 | |||
3d67607768 | |||
ce271649ac | |||
0078cb88c3 | |||
19cb548e18 | |||
b3cf7dbc14 | |||
bbfe0a0cd1 | |||
92b3f90380 | |||
b09345f2e1 | |||
0d41c60a38 | |||
5132f4850f | |||
53fae2939a | |||
14f248546a | |||
0d519b3d16 | |||
8e0a9d6d66 | |||
b8bc3476f4 | |||
6b326cfb79 | |||
aaef738dda | |||
35748fc1f3 | |||
a122d817e8 | |||
4ccce424de | |||
396a79899e | |||
de53681d2b | |||
aacd42b9f6 | |||
5ef5f9b45f | |||
98d420d5aa | |||
5b75818fc5 | |||
f49577bc77 | |||
a87c65872c | |||
ed636d5e2f | |||
2ced70652b | |||
dc742f4b8d | |||
689f2e7fbf | |||
ba1dca1826 | |||
a07e4c69b6 | |||
05adde552d | |||
0ddbfd1036 | |||
8b45df37d2 | |||
70e557575f | |||
bcbd541d48 | |||
6383a745ff | |||
b89d6644d8 | |||
8fbef4b4bb | |||
d9f5a97d56 | |||
e4ee03cb61 | |||
c2a65c71e1 | |||
8c456a2da4 | |||
f856386bf7 | |||
e1448eaeda | |||
3cf61f2f93 | |||
ff61282104 | |||
b2fe9d7d4d | |||
16a5e6c01a | |||
306b1d74bb | |||
a7e652f1f7 | |||
e1a3ab2726 | |||
ae9c412b6d | |||
fad0027e17 | |||
1d7f012fd1 | |||
ee4bf163ef | |||
fabdba4452 | |||
1a14fc5c48 | |||
fa5b64ce2e | |||
4397a44b80 | |||
d4bb092543 | |||
f73f0cc341 | |||
b95d0e2848 | |||
50b97fa28f | |||
61c7feca87 | |||
4dde6d1a31 | |||
9ac2f02885 | |||
19eb77f049 | |||
59276b7160 | |||
9062e40ec5 | |||
9f78c8f6b4 | |||
144d315e27 | |||
34dc85e605 | |||
4ba0f343e3 | |||
4876eaafcc | |||
16f6be3613 | |||
da0c7d7484 | |||
fbd2f86f00 | |||
a309b7a066 | |||
9ad9c275a4 | |||
7a467d43df | |||
db97250db8 | |||
c0452038f7 |
24
.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
* text=auto
|
||||||
|
* text eol=lf
|
||||||
|
|
||||||
|
# Windows forced line-endings
|
||||||
|
/.idea/* text eol=crlf
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
*.jar binary
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.webp binary
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.gz binary
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.woff binary
|
||||||
|
*.pyc binary
|
||||||
|
*.swp binary
|
37
.github/CONTRIBUTING.md
vendored
@ -1,19 +1,27 @@
|
|||||||
|
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
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (More > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Debug version is equal to the number of commits as seen in the main page
|
* 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 steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* For multipart issues use list like this:
|
* Don't group unrelated requests into one issue
|
||||||
* [x] Done
|
|
||||||
* [ ] Not done
|
|
||||||
```
|
|
||||||
* [x] Done
|
|
||||||
* [ ] Not done
|
|
||||||
```
|
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
@ -21,10 +29,5 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
|||||||
|
|
||||||
# Feature requests
|
# Feature requests
|
||||||
|
|
||||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
|
|
||||||
# Translations
|
|
||||||
|
|
||||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
|
||||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
|
||||||
|
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github: inorichi
|
||||||
|
ko_fi: inorichi
|
26
.github/ISSUE_TEMPLATE.md
vendored
@ -1,6 +1,26 @@
|
|||||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
Remove line above and describe your issue here. Fill out version below.
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app (stable is v0.10.5)
|
||||||
|
- 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**
|
||||||
|
|
||||||
---
|
---
|
||||||
Version: r000 or v0.0.0
|
|
||||||
|
## Device information
|
||||||
|
* Tachiyomi version: ?
|
||||||
|
* Android version: ?
|
||||||
|
* Device: ?
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
|
||||||
|
## Issue/Request
|
||||||
|
?
|
||||||
|
|
||||||
|
## Other details
|
||||||
|
Additional details and attachments.
|
||||||
|
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.10.5)
|
||||||
|
- 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.
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Tachiyomi help website
|
||||||
|
url: https://tachiyomi.org/help/
|
||||||
|
about: Common questions are answered here.
|
||||||
|
- name: Tachiyomi extensions GitHub repository
|
||||||
|
url: https://github.com/inorichi/tachiyomi-extensions
|
||||||
|
about: Issues about an extension/source/catalogue should be opened here instead.
|
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.10.5)
|
||||||
|
- 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)
|
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, invalid"
|
||||||
|
---
|
||||||
|
|
||||||
|
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
BIN
.github/readme-images/app-icon.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
.github/readme-images/screens.png
vendored
Normal file
After Width: | Height: | Size: 1.2 MiB |
6
.github/runner-files/ci-gradle.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.jvmargs=-Xmx5120m
|
||||||
|
org.gradle.workers.max=2
|
||||||
|
|
||||||
|
kotlin.incremental=false
|
||||||
|
kotlin.compiler.execution.strategy=in-process
|
95
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
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
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
|
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 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
|
||||||
|
- 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 == 'inorichi/tachiyomi'
|
||||||
|
id: get_tag_name
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/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: Create release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
|
release_name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload APK to release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
||||||
|
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
asset_content_type: application/vnd.android.package-archive
|
37
.github/workflows/issue_closer.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Issue closer
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoclose:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Autoclose when created in wrong repo
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: title
|
||||||
|
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
|
||||||
|
- name: Autoclose when no short description provided
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: title
|
||||||
|
regex: ".*<Write short description here>*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
||||||
|
- name: Autoclose when body acknowledgement section not removed
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: body
|
||||||
|
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
|
||||||
|
- name: Autoclose when body requested information not filled out
|
||||||
|
uses: arkon/issue-closer-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
type: body
|
||||||
|
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
||||||
|
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
9
.gitignore
vendored
@ -2,8 +2,15 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
|
||||||
.idea/
|
.idea/
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
# Built files
|
||||||
*/build
|
*/build
|
||||||
|
/build
|
||||||
|
*.apk
|
||||||
|
app/**/output.json
|
||||||
|
|
||||||
|
# Hebrew assets are copied on build
|
||||||
|
app/src/main/res/values-iw/
|
||||||
|
19
.travis.yml
@ -1,19 +0,0 @@
|
|||||||
language: android
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- platform-tools
|
|
||||||
- tools
|
|
||||||
|
|
||||||
# The BuildTools version used by your project
|
|
||||||
- build-tools-23.0.1
|
|
||||||
- android-23
|
|
||||||
- extra-android-m2repository
|
|
||||||
- extra-google-m2repository
|
|
||||||
- extra-android-support
|
|
||||||
- extra-google-google_play_services
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- chmod +x gradlew
|
|
||||||
#Build, and run tests
|
|
||||||
script: "./gradlew build testDebug"
|
|
||||||
sudo: false
|
|
28
PREVIEW_RELEASE_NOTES.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
### r1810
|
||||||
|
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
||||||
|
run properly. This includes app updates, library updates, and automatic backups.
|
||||||
|
|
||||||
|
### r1340
|
||||||
|
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
|
||||||
|
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
|
||||||
|
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
|
||||||
|
don't store anything.
|
||||||
|
|
||||||
|
### r959
|
||||||
|
- The download manager has been rewritten and it's possible some of your downloads
|
||||||
|
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
|
||||||
|
- You can now download to any folder in your SD card.
|
||||||
|
- The download directory setting has been reset.
|
||||||
|
|
||||||
|
### r857
|
||||||
|
- **Important!** Delete after read has been updated.
|
||||||
|
This means the value has been reset set to disabled.
|
||||||
|
This can be changed in Settings > Downloads
|
||||||
|
|
||||||
|
### r736
|
||||||
|
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
|
||||||
|
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
|
||||||
|
|
||||||
|
### r724
|
||||||
|
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
|
||||||
|
from the info tab, or clearing the database (the latter won't fix covers from library manga).
|
74
README.md
@ -1,24 +1,70 @@
|
|||||||
[](https://github.com/inorichi/tachiyomi/releases)
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
[](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
|
|-------|----------|---------|------------|---------|
|
||||||
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
|  | [](https://github.com/inorichi/tachiyomi/releases) | [](https://github.com/tachiyomiorg/android-app-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
|
||||||
|
|
||||||
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
# Tachiyomi
|
||||||
|
Tachiyomi is a free and open source manga reader for Android 5.0 and above.
|
||||||
|
|
||||||
Tachiyomi is a free and open source manga reader for Android.
|

|
||||||
|
|
||||||
Keep in mind it's still a beta, so expect it to crash sometimes.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Online and offline reading
|
Features include:
|
||||||
* Configurable reader with multiple viewers and settings
|
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||||
* MyAnimeList support
|
* Local reading of downloaded manga
|
||||||
* Resume from the next unread chapter
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* Chapter filtering
|
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||||
* Schedule searching for updates
|
|
||||||
* Categories to organize your library
|
* 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/inorichi/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/android-app-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/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)
|
||||||
|
|
||||||
|
</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)
|
||||||
|
* 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
|
||||||
|
|
||||||
|
</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)
|
||||||
|
|
||||||
|
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
[See our website.](https://tachiyomi.org/)
|
||||||
|
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
2
app/.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
custom.gradle
|
||||||
|
342
app/build.gradle
@ -1,18 +1,22 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'com.neenbedankt.android-apt'
|
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||||
apply plugin: 'me.tatarka.retrolambda'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||||
|
|
||||||
retrolambda {
|
shortcutHelper.filePath = './shortcuts.xml'
|
||||||
jvmArgs '-noverify'
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Git is needed in your system PATH for these commands to work.
|
// 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
|
// If it's not installed, you can return a random value as a workaround
|
||||||
getCommitCount = {
|
getCommitCount = {
|
||||||
return 'git rev-list --count origin/master'.execute().text.trim()
|
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||||
// return "1"
|
// return "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,42 +32,64 @@ ext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def includeUpdater() {
|
|
||||||
return hasProperty("include_updater");
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion AndroidConfig.compileSdk
|
||||||
buildToolsVersion "23.0.2"
|
buildToolsVersion AndroidConfig.buildTools
|
||||||
publishNonDefault true
|
ndkVersion AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion 23
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 5
|
versionCode 52
|
||||||
versionName "0.1.4"
|
versionName "0.10.6"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
||||||
|
|
||||||
|
multiDexEnabled true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
buildFeatures {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
viewBinding = true
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
postprocessing {
|
||||||
shrinkResources true
|
obfuscate false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
optimizeCode true
|
||||||
|
removeUnusedCode false
|
||||||
|
removeUnusedResources true
|
||||||
|
proguardFiles 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "default"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
standard {
|
||||||
|
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||||
|
dimension "default"
|
||||||
|
}
|
||||||
|
fdroid {
|
||||||
|
dimension "default"
|
||||||
|
}
|
||||||
|
dev {
|
||||||
|
resConfigs "en", "xxhdpi"
|
||||||
|
dimension "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,90 +101,228 @@ android {
|
|||||||
exclude 'META-INF/NOTICE'
|
exclude 'META-INF/NOTICE'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
disable 'ExtraTranslation'
|
||||||
|
|
||||||
abortOnError false
|
abortOnError false
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
apt {
|
kotlinOptions {
|
||||||
arguments {
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
eventBusIndex "eu.kanade.tachiyomi.EventBusIndex"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
final SUPPORT_LIBRARY_VERSION = '23.1.1'
|
|
||||||
final DAGGER_VERSION = '2.0.2'
|
|
||||||
final EVENTBUS_VERSION = '3.0.0'
|
|
||||||
final OKHTTP_VERSION = '3.1.1'
|
|
||||||
final STORIO_VERSION = '1.8.0'
|
|
||||||
final ICEPICK_VERSION = '3.1.0'
|
|
||||||
final MOCKITO_VERSION = '1.10.19'
|
|
||||||
|
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
compile project(":SubsamplingScaleImageView")
|
implementation 'tachiyomi.sourceapi:source-api:1.1'
|
||||||
compile project(":ReactiveNetwork")
|
|
||||||
|
|
||||||
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
// AndroidX libraries
|
||||||
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
|
||||||
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||||
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha01'
|
||||||
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.browser:browser:1.3.0'
|
||||||
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha1'
|
||||||
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||||
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
|
||||||
compile 'com.squareup.okio:okio:1.6.0'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
compile 'com.google.code.gson:gson:2.5'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
|
||||||
compile 'org.jsoup:jsoup:1.8.3'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||||
compile 'io.reactivex:rxandroid:1.1.0'
|
|
||||||
compile 'io.reactivex:rxjava:1.1.0'
|
|
||||||
compile 'com.squareup.retrofit:retrofit:1.9.0'
|
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
|
||||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
|
||||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
|
||||||
compile 'info.android15.nucleus:nucleus:2.0.4'
|
|
||||||
compile 'com.github.bumptech.glide:glide:3.6.1'
|
|
||||||
compile 'com.jakewharton:butterknife:7.0.1'
|
|
||||||
compile 'com.jakewharton.timber:timber:4.1.0'
|
|
||||||
compile 'ch.acra:acra:4.8.1'
|
|
||||||
compile "frankiesardo:icepick:$ICEPICK_VERSION"
|
|
||||||
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
|
|
||||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
|
||||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
|
||||||
compile 'com.nononsenseapps:filepicker:2.5.1'
|
|
||||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
|
||||||
|
|
||||||
compile "org.greenrobot:eventbus:$EVENTBUS_VERSION"
|
final lifecycle_version = '2.3.0-beta01'
|
||||||
apt "org.greenrobot:eventbus-annotation-processor:$EVENTBUS_VERSION"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
|
|
||||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
// Job scheduling
|
||||||
apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
implementation "androidx.work:work-runtime-ktx:2.5.0-beta02"
|
||||||
apt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
|
||||||
provided 'org.glassfish:javax.annotation:10.0-b28'
|
|
||||||
|
|
||||||
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
|
// UI library
|
||||||
transitive = true
|
implementation 'com.google.android.material:material:1.3.0-alpha04'
|
||||||
|
|
||||||
|
standardImplementation 'com.google.firebase:firebase-core:18.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
|
||||||
|
final okhttp_version = '4.10.0-RC1'
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
|
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||||
|
implementation 'com.squareup.okio:okio:2.9.0'
|
||||||
|
|
||||||
|
// TLS 1.3 support for Android < 10
|
||||||
|
implementation 'org.conscrypt:conscrypt-android:2.5.1'
|
||||||
|
|
||||||
|
// REST
|
||||||
|
final retrofit_version = '2.9.0'
|
||||||
|
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
|
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
|
||||||
|
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
|
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
final kotlin_serialization_version = '1.0.1'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialization_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlin_serialization_version"
|
||||||
|
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.3.0'
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||||
|
implementation 'com.github.junrar:junrar:7.4.0'
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation 'org.jsoup:jsoup:1.13.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 'io.requery:sqlite-android:3.33.0'
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
implementation 'com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3'
|
||||||
|
|
||||||
|
// 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.11.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"
|
||||||
|
|
||||||
|
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:6caf219'
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
implementation 'ch.acra:acra-http:5.7.0'
|
||||||
|
|
||||||
|
// 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.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.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||||
|
|
||||||
|
// FlowBinding
|
||||||
|
final flowbinding_version = '0.12.0'
|
||||||
|
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"
|
||||||
|
|
||||||
|
// Licenses
|
||||||
|
implementation "com.mikepenz:aboutlibraries:$BuildPluginsVersion.ABOUTLIB_PLUGIN"
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.assertj:assertj-core:3.16.1'
|
||||||
|
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-reflect:$BuildPluginsVersion.KOTLIN"
|
||||||
|
|
||||||
|
final coroutines_version = '1.4.1'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
|
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google material icons SVG.
|
buildscript {
|
||||||
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.4@aar') {
|
}
|
||||||
transitive = true
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
testCompile 'junit:junit:4.12'
|
mavenCentral()
|
||||||
testCompile 'org.assertj:assertj-core:2.3.0'
|
jcenter()
|
||||||
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
|
||||||
testCompile('org.robolectric:robolectric:3.0') {
|
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
|
||||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
androidTestApt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||||
|
tasks.withType(AbstractKotlinCompile).all {
|
||||||
|
kotlinOptions.freeCompilerArgs += [
|
||||||
|
"-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.serialization.ExperimentalSerializationApi",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||||
|
task copyHebrewStrings(type: Copy) {
|
||||||
|
from './src/main/res/values-he'
|
||||||
|
into './src/main/res/values-iw'
|
||||||
|
include '**/*'
|
||||||
|
}
|
||||||
|
|
||||||
|
preBuild.dependsOn(formatKotlin, copyHebrewStrings)
|
||||||
|
|
||||||
|
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
172
app/proguard-rules.pro
vendored
@ -1,66 +1,29 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
# Extensions may require methods unused in the core app
|
||||||
|
-dontwarn eu.kanade.tachiyomi.**
|
||||||
|
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
||||||
|
|
||||||
# Retrolambda
|
-keep class org.jsoup.** { *; }
|
||||||
-dontwarn java.lang.invoke.*
|
-keep class kotlin.** { *; }
|
||||||
|
|
||||||
# OkHttp
|
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keep class okhttp3.** { *; }
|
-keep class okhttp3.** { *; }
|
||||||
-keep interface okhttp3.** { *; }
|
-keep class com.google.gson.** { *; }
|
||||||
-dontwarn okhttp3.**
|
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||||
-dontwarn okio.**
|
-keep class com.squareup.duktape.** { *; }
|
||||||
|
|
||||||
# Okio
|
# Design library
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-dontwarn com.google.android.material.**
|
||||||
-dontwarn java.nio.file.*
|
-keep class com.google.android.material.** { *; }
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
-keep interface com.google.android.material.** { *; }
|
||||||
-dontwarn okio.**
|
-keep public class com.google.android.material.R$* { *; }
|
||||||
|
|
||||||
# ButterKnife 7
|
-keep class com.hippo.image.** { *; }
|
||||||
-keep class butterknife.** { *; }
|
-keep interface com.hippo.image.** { *; }
|
||||||
-dontwarn butterknife.internal.**
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
-keep class **$$ViewBinder { *; }
|
<init>();
|
||||||
|
|
||||||
-keepclasseswithmembernames class * {
|
|
||||||
@butterknife.* <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembernames class * {
|
|
||||||
@butterknife.* <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#Easy-Adapter v1.5.0
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keepclassmembers class * extends uk.co.ribot.easyadapter.ItemViewHolder {
|
|
||||||
public <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
## GreenRobot EventBus specific rules ##
|
|
||||||
# http://greenrobot.org/eventbus/documentation/proguard/
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keepclassmembers class ** {
|
|
||||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
|
||||||
}
|
|
||||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
|
||||||
|
|
||||||
# Only required if you use AsyncExecutor
|
|
||||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
|
||||||
<init>(java.lang.Throwable);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Glide specific rules #
|
|
||||||
# https://github.com/bumptech/glide
|
|
||||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
|
||||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
|
||||||
**[] $VALUES;
|
|
||||||
public *;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# 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* {
|
||||||
@ -76,98 +39,41 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retrofit 1.X
|
# ReactiveNetwork
|
||||||
|
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||||
|
|
||||||
-keep class com.squareup.okhttp.** { *; }
|
## GSON ##
|
||||||
-keep class retrofit.** { *; }
|
|
||||||
-keep interface com.squareup.okhttp.** { *; }
|
|
||||||
|
|
||||||
-dontwarn com.squareup.okhttp.**
|
|
||||||
-dontwarn okio.**
|
|
||||||
-dontwarn retrofit.**
|
|
||||||
-dontwarn rx.**
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@retrofit.http.* <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# If in your rest service interface you use methods with Callback argument.
|
|
||||||
-keepattributes Exceptions
|
|
||||||
|
|
||||||
# If your rest service methods throw custom exceptions, because you've defined an ErrorHandler.
|
|
||||||
-keepattributes Signature
|
|
||||||
|
|
||||||
# AppCombat
|
|
||||||
-keep public class android.support.v7.widget.** { *; }
|
|
||||||
-keep public class android.support.v7.internal.widget.** { *; }
|
|
||||||
-keep public class android.support.v7.internal.view.menu.** { *; }
|
|
||||||
|
|
||||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
|
||||||
public <init>(android.content.Context);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Icepick
|
|
||||||
-dontwarn icepick.**
|
|
||||||
-keep class **$$Icepick { *; }
|
|
||||||
-keepclasseswithmembernames class * {
|
|
||||||
@icepick.* <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
## GSON 2.2.4 specific rules ##
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
-keepattributes EnclosingMethod
|
|
||||||
|
|
||||||
# Gson specific classes
|
# Gson specific classes
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-keep class sun.misc.Unsafe { *; }
|
||||||
-keep class com.google.gson.stream.** { *; }
|
|
||||||
|
|
||||||
## ACRA 4.5.0 specific rules ##
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
|
|
||||||
# we need line numbers in our stack traces otherwise they are pretty useless
|
|
||||||
-renamesourcefileattribute SourceFile
|
|
||||||
-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# ACRA needs "annotations" so add this...
|
## kotlinx.serialization ##
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# keep this class so that logging will show 'ACRA' and not a obfuscated name like 'a'.
|
-keepattributes *Annotation*, InnerClasses
|
||||||
# Note: if you are removing log messages elsewhere in this file then this isn't necessary
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
-keep class org.acra.ACRA {
|
|
||||||
*;
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
# keep this around for some enums that ACRA needs
|
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
||||||
-keep class org.acra.ReportingInteractionMode {
|
-keepclassmembers class eu.kanade.tachiyomi.** {
|
||||||
*;
|
*** Companion;
|
||||||
}
|
}
|
||||||
|
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||||
-keepnames class org.acra.sender.HttpSender$** {
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
*;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-keepnames class org.acra.ReportField {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
|
|
||||||
# keep this otherwise it is removed by ProGuard
|
|
||||||
-keep public class org.acra.ErrorReporter {
|
|
||||||
public void addCustomData(java.lang.String,java.lang.String);
|
|
||||||
public void putCustomData(java.lang.String,java.lang.String);
|
|
||||||
public void removeCustomData(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
# keep this otherwise it is removed by ProGuard
|
|
||||||
-keep public class org.acra.ErrorReporter {
|
|
||||||
public void handleSilentException(java.lang.Throwable);
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep the support library
|
|
||||||
-keep class org.acra.** { *; }
|
|
||||||
-keep interface org.acra.** { *; }
|
|
47
app/shortcuts.xml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/sc_collections_bookmark_48dp"
|
||||||
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
|
android:shortcutId="show_library"
|
||||||
|
android:shortcutLongLabel="@string/label_library"
|
||||||
|
android:shortcutShortLabel="@string/label_library">
|
||||||
|
<intent
|
||||||
|
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
|
||||||
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/sc_new_releases_48dp"
|
||||||
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
|
android:shortcutId="show_recently_updated"
|
||||||
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
|
android:shortcutShortLabel="@string/short_recent_updates">
|
||||||
|
<intent
|
||||||
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/sc_history_48dp"
|
||||||
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
|
android:shortcutId="show_recently_read"
|
||||||
|
android:shortcutLongLabel="@string/label_recent_manga"
|
||||||
|
android:shortcutShortLabel="@string/label_recent_manga">
|
||||||
|
<intent
|
||||||
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
|
||||||
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/sc_explore_48dp"
|
||||||
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
|
android:shortcutId="show_catalogues"
|
||||||
|
android:shortcutLongLabel="@string/browse"
|
||||||
|
android:shortcutShortLabel="@string/browse">
|
||||||
|
<intent
|
||||||
|
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
|
||||||
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
</shortcut>
|
||||||
|
</shortcuts>
|
34
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108.0"
|
||||||
|
android:viewportHeight="108.0">
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#455A64"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#607D8B"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#CE2828"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#FFF"/>
|
||||||
|
<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:fillType="evenOdd"
|
||||||
|
android:fillColor="#000"/>
|
||||||
|
</vector>
|
5
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/transparent"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/transparent"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -4,81 +4,169 @@
|
|||||||
|
|
||||||
<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.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<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.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:hardwareAccelerated="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/AppTheme" >
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:theme="@style/Theme.Tachiyomi.Light"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity">
|
android:name=".ui.main.MainActivity"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/Theme.Splash">
|
||||||
<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" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
|
android:label="@string/process_text_action_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.kanade.tachiyomi.SEARCH" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:parentActivityName=".ui.manga.MangaActivity">
|
android:launchMode="singleTask">
|
||||||
<meta-data
|
<intent-filter>
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
android:value=".ui.manga.MangaActivity" />
|
</intent-filter>
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.SettingsActivity"
|
|
||||||
android:label="@string/label_settings"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.library.category.CategoryActivity"
|
|
||||||
android:label="@string/label_categories"
|
|
||||||
android:parentActivityName=".ui.main.MainActivity">
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
|
android:resource="@xml/s_pen_actions"/>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
android:name=".ui.security.BiometricUnlockActivity"
|
||||||
|
android:theme="@style/Theme.Splash" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.webview.WebViewActivity"
|
||||||
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
|
<activity
|
||||||
|
android:name=".widget.CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme">
|
android:theme="@style/FilePickerTheme" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
|
android:label="Anilist">
|
||||||
|
<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="anilist-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
|
android:configChanges="uiMode|orientation|screenSize" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
|
android:label="Shikimori">
|
||||||
|
<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="shikimori-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
|
android:label="Bangumi">
|
||||||
|
<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="bangumi-auth"
|
||||||
|
android:scheme="tachiyomi" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service android:name=".data.sync.LibraryUpdateService"
|
<activity
|
||||||
android:exported="false"/>
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
<service android:name=".data.download.DownloadService"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<service android:name=".data.sync.UpdateMangaSyncService"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".data.sync.LibraryUpdateService$SyncOnConnectionAvailable"
|
|
||||||
android:enabled="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".data.sync.LibraryUpdateAlarm">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:value="GlideModule" />
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".data.notification.NotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.library.LibraryUpdateService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.download.DownloadService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.updater.UpdaterService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupCreateService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".data.backup.BackupRestoreService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.annotation.ReportsCrashes;
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
|
|
||||||
import eu.kanade.tachiyomi.injection.component.AppComponent;
|
|
||||||
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
|
|
||||||
import eu.kanade.tachiyomi.injection.module.AppModule;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@ReportsCrashes(
|
|
||||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
|
||||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
|
||||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
|
||||||
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
|
|
||||||
)
|
|
||||||
public class App extends Application {
|
|
||||||
|
|
||||||
AppComponent applicationComponent;
|
|
||||||
ComponentReflectionInjector<AppComponent> componentInjector;
|
|
||||||
|
|
||||||
public static App get(Context context) {
|
|
||||||
return (App) context.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
|
|
||||||
|
|
||||||
applicationComponent = DaggerAppComponent.builder()
|
|
||||||
.appModule(new AppModule(this))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
componentInjector =
|
|
||||||
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
|
|
||||||
|
|
||||||
setupEventBus();
|
|
||||||
|
|
||||||
ACRA.init(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setupEventBus() {
|
|
||||||
EventBus.builder()
|
|
||||||
.addIndex(new EventBusIndex())
|
|
||||||
.logNoSubscriberMessages(false)
|
|
||||||
.installDefaultEventBus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AppComponent getComponent() {
|
|
||||||
return applicationComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needed to replace the component with a test specific one
|
|
||||||
public void setComponent(AppComponent applicationComponent) {
|
|
||||||
this.applicationComponent = applicationComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
|
|
||||||
return componentInjector;
|
|
||||||
}
|
|
||||||
}
|
|
93
app/src/main/java/eu/kanade/tachiyomi/App.kt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import androidx.multidex.MultiDex
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import org.acra.ACRA
|
||||||
|
import org.acra.annotation.AcraCore
|
||||||
|
import org.acra.annotation.AcraHttpSender
|
||||||
|
import org.acra.sender.HttpSender
|
||||||
|
import org.conscrypt.Conscrypt
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
import java.security.Security
|
||||||
|
|
||||||
|
@AcraCore(
|
||||||
|
buildConfigClass = BuildConfig::class,
|
||||||
|
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
||||||
|
)
|
||||||
|
@AcraHttpSender(
|
||||||
|
uri = "https://tachiyomi.kanade.eu/crash_report",
|
||||||
|
httpMethod = HttpSender.Method.PUT
|
||||||
|
)
|
||||||
|
open class App : Application(), LifecycleObserver {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
|
// Debug tool; see https://fbflipper.com/
|
||||||
|
// SoLoader.init(this, false)
|
||||||
|
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||||
|
// val client = AndroidFlipperClient.getInstance(this)
|
||||||
|
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||||
|
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||||
|
// client.start()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TLS 1.3 support for Android < 10
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
|
setupAcra()
|
||||||
|
setupNotificationChannels()
|
||||||
|
|
||||||
|
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||||
|
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
MultiDex.install(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||||
|
@Suppress("unused")
|
||||||
|
fun onAppBackgrounded() {
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
if (preferences.lockAppAfter().get() >= 0) {
|
||||||
|
SecureActivityDelegate.locked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun setupAcra() {
|
||||||
|
ACRA.init(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun setupNotificationChannels() {
|
||||||
|
Notifications.createChannels(this)
|
||||||
|
}
|
||||||
|
}
|
62
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { ChapterCache(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { CoverCache(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { NetworkHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
||||||
|
|
||||||
|
addSingletonFactory { ExtensionManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
|
||||||
|
GlobalScope.launch { get<PreferencesHelper>() }
|
||||||
|
|
||||||
|
GlobalScope.launch { get<NetworkHelper>() }
|
||||||
|
|
||||||
|
GlobalScope.launch { get<SourceManager>() }
|
||||||
|
|
||||||
|
GlobalScope.launch { get<DatabaseHelper>() }
|
||||||
|
|
||||||
|
GlobalScope.launch { get<DownloadManager>() }
|
||||||
|
}
|
||||||
|
}
|
127
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
|
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.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object Migrations {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a migration when the application is updated.
|
||||||
|
*
|
||||||
|
* @param preferences Preferences of the application.
|
||||||
|
* @return true if a migration is performed, false otherwise.
|
||||||
|
*/
|
||||||
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
|
val context = preferences.context
|
||||||
|
|
||||||
|
// Cancel app updater job for debug builds that don't include it
|
||||||
|
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.cancelTask(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
// Fresh install
|
||||||
|
if (oldVersion == 0) {
|
||||||
|
// Set up default background tasks
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 14) {
|
||||||
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
if (oldVersion < 15) {
|
||||||
|
// Delete internal chapter cache dir.
|
||||||
|
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||||
|
}
|
||||||
|
if (oldVersion < 19) {
|
||||||
|
// Move covers to external files dir.
|
||||||
|
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
|
||||||
|
if (oldDir.exists()) {
|
||||||
|
val destDir = context.getExternalFilesDir("covers")
|
||||||
|
if (destDir != null) {
|
||||||
|
oldDir.listFiles()?.forEach {
|
||||||
|
it.renameTo(File(destDir, it.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 26) {
|
||||||
|
// Delete external chapter cache dir.
|
||||||
|
val extCache = context.externalCacheDir
|
||||||
|
if (extCache != null) {
|
||||||
|
val chapterCache = File(extCache, "chapter_disk_cache")
|
||||||
|
if (chapterCache.exists()) {
|
||||||
|
chapterCache.deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 43) {
|
||||||
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
|
||||||
|
// New extension update check job
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
if (oldVersion < 44) {
|
||||||
|
// Reset sorting preference if using removed sort by source
|
||||||
|
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||||
|
preferences.librarySortingMode().set(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force MAL log out due to login flow change
|
||||||
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
trackManager.myAnimeList.logout()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Nsfw
|
@ -0,0 +1,88 @@
|
|||||||
|
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.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.util.chapter.syncChaptersWithSource
|
||||||
|
import rx.Observable
|
||||||
|
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()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters list of chapters in the backup
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
internal fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return source.fetchChapterList(manga)
|
||||||
|
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||||
|
.doOnNext { (first) ->
|
||||||
|
if (first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
updateChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return number of backups.
|
||||||
|
*
|
||||||
|
* @return number of backups selected by user
|
||||||
|
*/
|
||||||
|
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
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 kotlinx.coroutines.Job
|
||||||
|
import rx.Observable
|
||||||
|
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 fun performRestore(uri: Uri): Boolean
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
internal fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
val errorMessage = if (it is NoChaptersException) {
|
||||||
|
context.getString(R.string.no_chapters_error)
|
||||||
|
} else {
|
||||||
|
it.message
|
||||||
|
}
|
||||||
|
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
internal 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} - ${context.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
|
||||||
|
*/
|
||||||
|
internal fun showRestoreProgress(
|
||||||
|
progress: Int,
|
||||||
|
amount: Int,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
notifier.showRestoreProgress(title, progress, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(context.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,16 @@
|
|||||||
|
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>)
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
object BackupConst {
|
||||||
|
|
||||||
|
private const val NAME = "BackupRestoreServices"
|
||||||
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||||
|
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
||||||
|
|
||||||
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for backing up library information to a JSON file.
|
||||||
|
*/
|
||||||
|
class BackupCreateService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Filter options
|
||||||
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
internal const val BACKUP_CHAPTER = 0x2
|
||||||
|
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
internal const val BACKUP_HISTORY = 0x4
|
||||||
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
internal const val BACKUP_TRACK = 0x8
|
||||||
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean =
|
||||||
|
context.isServiceRunning(BackupCreateService::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a backup from library
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param flags determines what to backup
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
|
putExtra(BackupConst.EXTRA_TYPE, type)
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
notifier = BackupNotifier(this)
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
|
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(name: Intent?): Boolean {
|
||||||
|
destroyJob()
|
||||||
|
return super.stopService(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyJob()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyJob() {
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
|
try {
|
||||||
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||||
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
|
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
||||||
|
val backupManager = when (backupType) {
|
||||||
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
||||||
|
else -> LegacyBackupManager(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||||
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
|
notifier.showBackupComplete(unifile)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
notifier.showBackupError(e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSelf(startId)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val uri = preferences.backupsDirectory().get().toUri()
|
||||||
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
|
return try {
|
||||||
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
if (preferences.createLegacyBackup().get()) {
|
||||||
|
LegacyBackupManager(context).createBackup(uri, flags, true)
|
||||||
|
}
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BackupCreator"
|
||||||
|
|
||||||
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
|
if (interval > 0) {
|
||||||
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
|
interval.toLong(),
|
||||||
|
TimeUnit.HOURS,
|
||||||
|
10,
|
||||||
|
TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
.addTag(TAG)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
|
} else {
|
||||||
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BackupNotifier(private val context: Context) {
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
|
||||||
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
setAutoCancel(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationCompat.Builder.show(id: Int) {
|
||||||
|
context.notificationManager.notify(id, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupProgress(): NotificationCompat.Builder {
|
||||||
|
val builder = with(progressNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupError(error: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.creating_backup_error))
|
||||||
|
setContentText(error)
|
||||||
|
|
||||||
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBackupComplete(unifile: UniFile) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.backup_created))
|
||||||
|
setContentText(unifile.filePath ?: unifile.name)
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_share_24dp,
|
||||||
|
context.getString(R.string.action_share),
|
||||||
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
)
|
||||||
|
|
||||||
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
|
||||||
|
val builder = with(progressNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restoring_backup))
|
||||||
|
|
||||||
|
if (!preferences.hideNotificationContent()) {
|
||||||
|
setContentText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(maxAmount, progress, false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.ic_close_24dp,
|
||||||
|
context.getString(R.string.action_stop),
|
||||||
|
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreError(error: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restoring_backup_error))
|
||||||
|
setContentText(error)
|
||||||
|
|
||||||
|
show(Notifications.ID_RESTORE_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||||
|
|
||||||
|
val timeString = context.getString(
|
||||||
|
R.string.restore_duration,
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with(completeNotificationBuilder) {
|
||||||
|
setContentTitle(context.getString(R.string.restore_completed))
|
||||||
|
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||||
|
|
||||||
|
// Clear old actions if they exist
|
||||||
|
if (mActions.isNotEmpty()) {
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||||
|
val destFile = File(path, file)
|
||||||
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
|
addAction(
|
||||||
|
R.drawable.nnf_ic_file_folder,
|
||||||
|
context.getString(R.string.action_open_log),
|
||||||
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
show(Notifications.ID_RESTORE_COMPLETE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores backup.
|
||||||
|
*/
|
||||||
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean =
|
||||||
|
context.isServiceRunning(BackupRestoreService::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a service to restore a backup from Json
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
|
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
|
|
||||||
|
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
private var backupRestore: AbstractBackupRestore<*>? = null
|
||||||
|
private lateinit var notifier: BackupNotifier
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
notifier = BackupNotifier(this)
|
||||||
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
|
|
||||||
|
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService(name: Intent?): Boolean {
|
||||||
|
destroyJob()
|
||||||
|
return super.stopService(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyJob()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyJob() {
|
||||||
|
backupRestore?.job?.cancel()
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service receives an intent.
|
||||||
|
*
|
||||||
|
* @param intent the start intent from.
|
||||||
|
* @param flags the flags of the command.
|
||||||
|
* @param startId the start id of this command.
|
||||||
|
* @return the start value of the command.
|
||||||
|
*/
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
|
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||||
|
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
||||||
|
|
||||||
|
// Cancel any previous job if needed.
|
||||||
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
|
backupRestore = when (mode) {
|
||||||
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||||
|
else -> LegacyBackupRestore(this, notifier)
|
||||||
|
}
|
||||||
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
|
Timber.e(exception)
|
||||||
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
|
notifier.showRestoreError(exception.message)
|
||||||
|
stopSelf(startId)
|
||||||
|
}
|
||||||
|
backupRestore?.job = GlobalScope.launch(handler) {
|
||||||
|
if (backupRestore?.restoreBackup(uri) == false) {
|
||||||
|
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backupRestore?.job?.invokeOnCompletion {
|
||||||
|
stopSelf(startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,412 @@
|
|||||||
|
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 eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [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, online: Boolean): Observable<Manga> {
|
||||||
|
return if (online && source != null) {
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
|
.map { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
manga.favorite = manga.favorite
|
||||||
|
manga.initialized = true
|
||||||
|
manga.id = insertManga(manga)
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
.map {
|
||||||
|
it.initialized = it.description != null
|
||||||
|
it.id = insertManga(it)
|
||||||
|
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 = mutableListOf<MangaCategory>()
|
||||||
|
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 = mutableListOf<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 = mutableListOf<Track>()
|
||||||
|
|
||||||
|
tracks.forEach { track ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filter the chapters that couldn't be found.
|
||||||
|
chapters.filter { it.id != null }
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
updateChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||||
|
if (pos != -1) {
|
||||||
|
val dbChapter = dbChapters[pos]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapters.map { it.manga_id = manga.id }
|
||||||
|
|
||||||
|
updateChapters(chapters.filter { it.id != null })
|
||||||
|
insertChapters(chapters.filter { it.id == null })
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
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 eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
|
|
||||||
|
override 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, online)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>, online: Boolean) {
|
||||||
|
val manga = backupManga.getMangaImpl()
|
||||||
|
val chapters = backupManga.getChaptersImpl()
|
||||||
|
val categories = backupManga.categories
|
||||||
|
val history = backupManga.history
|
||||||
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null || !online) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
} else {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} 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<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
db.inTransaction {
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
||||||
|
} 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, backupCategories, online)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [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<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
backupManager.restoreMangaFetchObservable(source, manga, online)
|
||||||
|
.doOnError {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap {
|
||||||
|
if (online && source != null) {
|
||||||
|
chapterFetchObservable(source, it, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
backupManager.restoreChaptersForMangaOffline(it, chapters)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks, backupCategories)
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
trackingFetchObservable(it, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreMangaNoFetch(
|
||||||
|
source: Source?,
|
||||||
|
backupManga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
online: Boolean
|
||||||
|
) {
|
||||||
|
Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (online && source != null) {
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backupManager.restoreChaptersForMangaOffline(manga, chapters)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
restoreExtraForManga(it, categories, history, tracks, backupCategories)
|
||||||
|
}
|
||||||
|
.flatMap { manga ->
|
||||||
|
trackingFetchObservable(manga, tracks)
|
||||||
|
}
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
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 kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
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 { it.name }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return Results(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
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(),
|
||||||
|
)
|
@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
|
)
|
@ -0,0 +1,87 @@
|
|||||||
|
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,
|
||||||
|
// @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(),
|
||||||
|
) {
|
||||||
|
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 = 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.viewer,
|
||||||
|
chapterFlags = manga.chapter_flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.full.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
|
||||||
|
@Serializer(forClass = Backup::class)
|
||||||
|
object BackupSerializer
|
@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,434 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
|
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.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.legacy.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||||
|
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.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
|
var parserVersion: Int = version
|
||||||
|
private set
|
||||||
|
|
||||||
|
var parser: Gson = initParser()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set version of parser
|
||||||
|
*
|
||||||
|
* @param version version of parser
|
||||||
|
*/
|
||||||
|
internal fun setVersion(version: Int) {
|
||||||
|
this.parserVersion = version
|
||||||
|
parser = initParser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initParser(): Gson = when (parserVersion) {
|
||||||
|
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("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): 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 {
|
||||||
|
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 {
|
||||||
|
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+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
dir.createFile(Backup.getDefaultFilename())
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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 = mutableListOf<MangaCategory>()
|
||||||
|
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 = mutableListOf<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 = mutableListOf<Track>()
|
||||||
|
|
||||||
|
tracks.forEach { track ->
|
||||||
|
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 }
|
||||||
|
|
||||||
|
updateChapters(chapters)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
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.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.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
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.source.Source
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
|
|
||||||
|
override fun performRestore(uri: Uri): Boolean {
|
||||||
|
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||||
|
val json = JsonParser.parseReader(reader).asJsonObject
|
||||||
|
|
||||||
|
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
||||||
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
mangasJson.forEach {
|
||||||
|
if (job?.isActive != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreManga(it.asJsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy
|
||||||
|
|
||||||
|
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.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
|
||||||
|
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 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangas = mangasJson.asJsonArray
|
||||||
|
if (mangas.size() == 0) {
|
||||||
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = getSourceMapping(json)
|
||||||
|
val missingSources = sources
|
||||||
|
.filter { sourceManager.get(it.key) == null }
|
||||||
|
.values
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
val trackers = mangas
|
||||||
|
.filter { it.asJsonObject.has("track") }
|
||||||
|
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||||
|
.map { it.asJsonObject["s"].asInt }
|
||||||
|
.distinct()
|
||||||
|
val missingTrackers = trackers
|
||||||
|
.mapNotNull { trackManager.getService(it) }
|
||||||
|
.filter { !it.isLogged }
|
||||||
|
.map { it.name }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
return Results(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.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"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
|
data class DHistory(val url: String, val lastRead: Long)
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.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.legacy.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.legacy.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.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.legacy.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.legacy.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,268 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.text.format.Formatter;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.jakewharton.disklrucache.DiskLruCache;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okio.BufferedSink;
|
|
||||||
import okio.Okio;
|
|
||||||
import rx.Observable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to create chapter cache
|
|
||||||
* For each image in a chapter a file is created
|
|
||||||
* For each chapter a Json list is created and converted to a file.
|
|
||||||
* The files are in format *md5key*.0
|
|
||||||
*/
|
|
||||||
public class ChapterCache {
|
|
||||||
|
|
||||||
/** Name of cache directory. */
|
|
||||||
private static final String PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache";
|
|
||||||
|
|
||||||
/** Application cache version. */
|
|
||||||
private static final int PARAMETER_APP_VERSION = 1;
|
|
||||||
|
|
||||||
/** The number of values per cache entry. Must be positive. */
|
|
||||||
private static final int PARAMETER_VALUE_COUNT = 1;
|
|
||||||
|
|
||||||
/** The maximum number of bytes this cache should use to store. */
|
|
||||||
private static final int PARAMETER_CACHE_SIZE = 75 * 1024 * 1024;
|
|
||||||
|
|
||||||
/** Interface to global information about an application environment. */
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
/** Google Json class used for parsing JSON files. */
|
|
||||||
private final Gson gson;
|
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
|
||||||
private DiskLruCache diskCache;
|
|
||||||
|
|
||||||
/** Page list collection used for deserializing from JSON. */
|
|
||||||
private final Type pageListCollection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor of ChapterCache.
|
|
||||||
* @param context application environment interface.
|
|
||||||
*/
|
|
||||||
public ChapterCache(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
// Initialize Json handler.
|
|
||||||
gson = new Gson();
|
|
||||||
|
|
||||||
// Try to open cache in default cache directory.
|
|
||||||
try {
|
|
||||||
diskCache = DiskLruCache.open(
|
|
||||||
new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY),
|
|
||||||
PARAMETER_APP_VERSION,
|
|
||||||
PARAMETER_VALUE_COUNT,
|
|
||||||
PARAMETER_CACHE_SIZE
|
|
||||||
);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
pageListCollection = new TypeToken<List<Page>>() {}.getType();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns directory of cache.
|
|
||||||
* @return directory of cache.
|
|
||||||
*/
|
|
||||||
public File getCacheDir() {
|
|
||||||
return diskCache.getDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns real size of directory.
|
|
||||||
* @return real size of directory.
|
|
||||||
*/
|
|
||||||
private long getRealSize() {
|
|
||||||
return DiskUtils.getDirectorySize(getCacheDir());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns real size of directory in human readable format.
|
|
||||||
* @return real size of directory.
|
|
||||||
*/
|
|
||||||
public String getReadableSize() {
|
|
||||||
return Formatter.formatFileSize(context, getRealSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove file from cache.
|
|
||||||
* @param file name of file "md5.0".
|
|
||||||
* @return status of deletion for the file.
|
|
||||||
*/
|
|
||||||
public boolean removeFileFromCache(String file) {
|
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
|
||||||
if (file.equals("journal") || file.startsWith("journal."))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Remove the extension from the file to get the key of the cache
|
|
||||||
String key = file.substring(0, file.lastIndexOf("."));
|
|
||||||
// Remove file from cache.
|
|
||||||
return diskCache.remove(key);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get page list from cache.
|
|
||||||
* @param chapterUrl the url of the chapter.
|
|
||||||
* @return an observable of the list of pages.
|
|
||||||
*/
|
|
||||||
public Observable<List<Page>> getPageListFromCache(final String chapterUrl) {
|
|
||||||
return Observable.fromCallable(() -> {
|
|
||||||
// Initialize snapshot (a snapshot of the values for an entry).
|
|
||||||
DiskLruCache.Snapshot snapshot = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create md5 key and retrieve snapshot.
|
|
||||||
String key = DiskUtils.hashKeyForDisk(chapterUrl);
|
|
||||||
snapshot = diskCache.get(key);
|
|
||||||
|
|
||||||
// Convert JSON string to list of objects.
|
|
||||||
return gson.fromJson(snapshot.getString(0), pageListCollection);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (snapshot != null) {
|
|
||||||
snapshot.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add page list to disk cache.
|
|
||||||
* @param chapterUrl the url of the chapter.
|
|
||||||
* @param pages list of pages.
|
|
||||||
*/
|
|
||||||
public void putPageListToCache(final String chapterUrl, final List<Page> pages) {
|
|
||||||
// Convert list of pages to json string.
|
|
||||||
String cachedValue = gson.toJson(pages);
|
|
||||||
|
|
||||||
// Initialize the editor (edits the values for an entry).
|
|
||||||
DiskLruCache.Editor editor = null;
|
|
||||||
|
|
||||||
// Initialize OutputStream.
|
|
||||||
OutputStream outputStream = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get editor from md5 key.
|
|
||||||
String key = DiskUtils.hashKeyForDisk(chapterUrl);
|
|
||||||
editor = diskCache.edit(key);
|
|
||||||
if (editor == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write chapter urls to cache.
|
|
||||||
outputStream = new BufferedOutputStream(editor.newOutputStream(0));
|
|
||||||
outputStream.write(cachedValue.getBytes());
|
|
||||||
outputStream.flush();
|
|
||||||
|
|
||||||
diskCache.flush();
|
|
||||||
editor.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do Nothing.
|
|
||||||
} finally {
|
|
||||||
if (editor != null) {
|
|
||||||
editor.abortUnlessCommitted();
|
|
||||||
}
|
|
||||||
if (outputStream != null) {
|
|
||||||
try {
|
|
||||||
outputStream.close();
|
|
||||||
} catch (IOException ignore) {
|
|
||||||
// Do Nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if image is in cache.
|
|
||||||
* @param imageUrl url of image.
|
|
||||||
* @return true if in cache otherwise false.
|
|
||||||
*/
|
|
||||||
public boolean isImageInCache(final String imageUrl) {
|
|
||||||
try {
|
|
||||||
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get image path from url.
|
|
||||||
* @param imageUrl url of image.
|
|
||||||
* @return path of image.
|
|
||||||
*/
|
|
||||||
public String getImagePath(final String imageUrl) {
|
|
||||||
try {
|
|
||||||
// Get file from md5 key.
|
|
||||||
String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0";
|
|
||||||
File file = new File(diskCache.getDirectory(), imageName);
|
|
||||||
return file.getCanonicalPath();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add image to cache.
|
|
||||||
* @param imageUrl url of image.
|
|
||||||
* @param response http response from page.
|
|
||||||
* @throws IOException image error.
|
|
||||||
*/
|
|
||||||
public void putImageToCache(final String imageUrl, final Response response) throws IOException {
|
|
||||||
// Initialize editor (edits the values for an entry).
|
|
||||||
DiskLruCache.Editor editor = null;
|
|
||||||
|
|
||||||
// Initialize BufferedSink (used for small writes).
|
|
||||||
BufferedSink sink = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get editor from md5 key.
|
|
||||||
String key = DiskUtils.hashKeyForDisk(imageUrl);
|
|
||||||
editor = diskCache.edit(key);
|
|
||||||
if (editor == null) {
|
|
||||||
throw new IOException("Unable to edit key");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize OutputStream and write image.
|
|
||||||
OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0));
|
|
||||||
sink = Okio.buffer(Okio.sink(outputStream));
|
|
||||||
sink.writeAll(response.body().source());
|
|
||||||
|
|
||||||
diskCache.flush();
|
|
||||||
editor.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
response.body().close();
|
|
||||||
throw new IOException("Unable to save image");
|
|
||||||
} finally {
|
|
||||||
if (editor != null) {
|
|
||||||
editor.abortUnlessCommitted();
|
|
||||||
}
|
|
||||||
if (sink != null) {
|
|
||||||
sink.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
205
app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
vendored
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to create chapter cache
|
||||||
|
* For each image in a chapter a file is created
|
||||||
|
* For each chapter a Json list is created and converted to a file.
|
||||||
|
* The files are in format *md5key*.0
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @constructor creates an instance of the chapter cache.
|
||||||
|
*/
|
||||||
|
class ChapterCache(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Name of cache directory. */
|
||||||
|
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
|
||||||
|
|
||||||
|
/** Application cache version. */
|
||||||
|
const val PARAMETER_APP_VERSION = 1
|
||||||
|
|
||||||
|
/** The number of values per cache entry. Must be positive. */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Google Json class used for parsing JSON files. */
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
|
/** Cache class used for cache management. */
|
||||||
|
private val diskCache = DiskLruCache.open(
|
||||||
|
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
|
||||||
|
PARAMETER_APP_VERSION,
|
||||||
|
PARAMETER_VALUE_COUNT,
|
||||||
|
PARAMETER_CACHE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns directory of cache.
|
||||||
|
*/
|
||||||
|
val cacheDir: File
|
||||||
|
get() = diskCache.directory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns real size of directory.
|
||||||
|
*/
|
||||||
|
private val realSize: Long
|
||||||
|
get() = DiskUtil.getDirectorySize(cacheDir)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns real size of directory in human readable format.
|
||||||
|
*/
|
||||||
|
val readableSize: String
|
||||||
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter.
|
||||||
|
* @return an observable of the list of pages.
|
||||||
|
*/
|
||||||
|
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||||
|
return Observable.fromCallable {
|
||||||
|
// Get the key for the chapter.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
|
|
||||||
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
|
diskCache.get(key).use {
|
||||||
|
gson.fromJson<List<Page>>(it.getString(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add page list to disk cache.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter.
|
||||||
|
* @param pages list of pages.
|
||||||
|
*/
|
||||||
|
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||||
|
// Convert list of pages to json string.
|
||||||
|
val cachedValue = gson.toJson(pages)
|
||||||
|
|
||||||
|
// Initialize the editor (edits the values for an entry).
|
||||||
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get editor from md5 key.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
|
// Write chapter urls to cache.
|
||||||
|
editor.newOutputStream(0).sink().buffer().use {
|
||||||
|
it.write(cachedValue.toByteArray())
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
diskCache.flush()
|
||||||
|
editor.commit()
|
||||||
|
editor.abortUnlessCommitted()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore.
|
||||||
|
} finally {
|
||||||
|
editor?.abortUnlessCommitted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if image is in cache.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of image.
|
||||||
|
* @return true if in cache otherwise false.
|
||||||
|
*/
|
||||||
|
fun isImageInCache(imageUrl: String): Boolean {
|
||||||
|
return try {
|
||||||
|
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||||
|
} catch (e: IOException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image file from url.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of image.
|
||||||
|
* @return path of image.
|
||||||
|
*/
|
||||||
|
fun getImageFile(imageUrl: String): File {
|
||||||
|
// Get file from md5 key.
|
||||||
|
val imageName = DiskUtil.hashKeyForDisk(imageUrl) + ".0"
|
||||||
|
return File(diskCache.directory, imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add image to cache.
|
||||||
|
*
|
||||||
|
* @param imageUrl url of image.
|
||||||
|
* @param response http response from page.
|
||||||
|
* @throws IOException image error.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun putImageToCache(imageUrl: String, response: Response) {
|
||||||
|
// Initialize editor (edits the values for an entry).
|
||||||
|
var editor: DiskLruCache.Editor? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get editor from md5 key.
|
||||||
|
val key = DiskUtil.hashKeyForDisk(imageUrl)
|
||||||
|
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||||
|
|
||||||
|
// Get OutputStream and write image with Okio.
|
||||||
|
response.body!!.source().saveTo(editor.newOutputStream(0))
|
||||||
|
|
||||||
|
diskCache.flush()
|
||||||
|
editor.commit()
|
||||||
|
} finally {
|
||||||
|
response.body?.close()
|
||||||
|
editor?.abortUnlessCommitted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKey(chapter: Chapter): String {
|
||||||
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
|
}
|
||||||
|
}
|
@ -1,235 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl;
|
|
||||||
import com.bumptech.glide.load.model.LazyHeaders;
|
|
||||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
|
||||||
import com.bumptech.glide.request.target.SimpleTarget;
|
|
||||||
import com.bumptech.glide.signature.StringSignature;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to create cover cache
|
|
||||||
* It is used to store the covers of the library.
|
|
||||||
* Makes use of Glide (which can avoid repeating requests) to download covers.
|
|
||||||
* Names of files are created with the md5 of the thumbnail URL
|
|
||||||
*/
|
|
||||||
public class CoverCache {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of cache directory.
|
|
||||||
*/
|
|
||||||
private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to global information about an application environment.
|
|
||||||
*/
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache directory used for cache management.
|
|
||||||
*/
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor of CoverCache.
|
|
||||||
*
|
|
||||||
* @param context application environment interface.
|
|
||||||
*/
|
|
||||||
public CoverCache(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
// Get cache directory from parameter.
|
|
||||||
cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
|
|
||||||
|
|
||||||
// Create cache directory.
|
|
||||||
createCacheDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create cache directory if it doesn't exist
|
|
||||||
*
|
|
||||||
* @return true if cache dir is created otherwise false.
|
|
||||||
*/
|
|
||||||
private boolean createCacheDir() {
|
|
||||||
return !cacheDir.exists() && cacheDir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the cover with Glide and save the file in this cache.
|
|
||||||
*
|
|
||||||
* @param thumbnailUrl url of thumbnail.
|
|
||||||
* @param headers headers included in Glide request.
|
|
||||||
*/
|
|
||||||
public void save(String thumbnailUrl, LazyHeaders headers) {
|
|
||||||
save(thumbnailUrl, headers, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the cover with Glide and save the file.
|
|
||||||
*
|
|
||||||
* @param thumbnailUrl url of thumbnail.
|
|
||||||
* @param headers headers included in Glide request.
|
|
||||||
* @param imageView imageView where picture should be displayed.
|
|
||||||
*/
|
|
||||||
private void save(String thumbnailUrl, LazyHeaders headers, @Nullable ImageView imageView) {
|
|
||||||
// Check if url is empty.
|
|
||||||
if (TextUtils.isEmpty(thumbnailUrl))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Download the cover with Glide and save the file.
|
|
||||||
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
|
|
||||||
Glide.with(context)
|
|
||||||
.load(url)
|
|
||||||
.downloadOnly(new SimpleTarget<File>() {
|
|
||||||
@Override
|
|
||||||
public void onResourceReady(File resource, GlideAnimation<? super File> anim) {
|
|
||||||
try {
|
|
||||||
// Copy the cover from Glide's cache to local cache.
|
|
||||||
copyToLocalCache(thumbnailUrl, resource);
|
|
||||||
|
|
||||||
// Check if imageView isn't null and show picture in imageView.
|
|
||||||
if (imageView != null) {
|
|
||||||
loadFromCache(imageView, resource);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the cover from Glide's cache to this cache.
|
|
||||||
*
|
|
||||||
* @param thumbnailUrl url of thumbnail.
|
|
||||||
* @param source the cover image.
|
|
||||||
* @throws IOException exception returned
|
|
||||||
*/
|
|
||||||
public void copyToLocalCache(String thumbnailUrl, File source) throws IOException {
|
|
||||||
// Create cache directory if needed.
|
|
||||||
createCacheDir();
|
|
||||||
|
|
||||||
// Get destination file.
|
|
||||||
File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
|
||||||
|
|
||||||
// Delete the current file if it exists.
|
|
||||||
if (dest.exists())
|
|
||||||
dest.delete();
|
|
||||||
|
|
||||||
// Write thumbnail image to file.
|
|
||||||
InputStream in = new FileInputStream(source);
|
|
||||||
try {
|
|
||||||
OutputStream out = new FileOutputStream(dest);
|
|
||||||
try {
|
|
||||||
// Transfer bytes from in to out.
|
|
||||||
byte[] buf = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = in.read(buf)) > 0) {
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the cover from cache.
|
|
||||||
*
|
|
||||||
* @param thumbnailUrl the thumbnail url.
|
|
||||||
* @return cover image.
|
|
||||||
*/
|
|
||||||
private File getCoverFromCache(String thumbnailUrl) {
|
|
||||||
return new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the cover file from the cache.
|
|
||||||
*
|
|
||||||
* @param thumbnailUrl the thumbnail url.
|
|
||||||
* @return status of deletion.
|
|
||||||
*/
|
|
||||||
public boolean deleteCoverFromCache(String thumbnailUrl) {
|
|
||||||
// Check if url is empty.
|
|
||||||
if (TextUtils.isEmpty(thumbnailUrl))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Remove file.
|
|
||||||
File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
|
|
||||||
return file.exists() && file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save or load the image from cache
|
|
||||||
*
|
|
||||||
* @param imageView imageView where picture should be displayed.
|
|
||||||
* @param thumbnailUrl the thumbnail url.
|
|
||||||
* @param headers headers included in Glide request.
|
|
||||||
*/
|
|
||||||
public void saveOrLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
|
|
||||||
// If file exist load it otherwise save it.
|
|
||||||
File localCover = getCoverFromCache(thumbnailUrl);
|
|
||||||
if (localCover.exists()) {
|
|
||||||
loadFromCache(imageView, localCover);
|
|
||||||
} else {
|
|
||||||
save(thumbnailUrl, headers, imageView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to load the cover from the cache directory into the specified image view.
|
|
||||||
* Glide stores the resized image in its cache to improve performance.
|
|
||||||
*
|
|
||||||
* @param imageView imageView where picture should be displayed.
|
|
||||||
* @param file file to load. Must exist!.
|
|
||||||
*/
|
|
||||||
private void loadFromCache(ImageView imageView, File file) {
|
|
||||||
Glide.with(context)
|
|
||||||
.load(file)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESULT)
|
|
||||||
.centerCrop()
|
|
||||||
.signature(new StringSignature(String.valueOf(file.lastModified())))
|
|
||||||
.into(imageView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to load the cover from network into the specified image view.
|
|
||||||
* The source image is stored in Glide's cache so that it can be easily copied to this cache
|
|
||||||
* if the manga is added to the library.
|
|
||||||
*
|
|
||||||
* @param imageView imageView where picture should be displayed.
|
|
||||||
* @param thumbnailUrl url of thumbnail.
|
|
||||||
* @param headers headers included in Glide request.
|
|
||||||
*/
|
|
||||||
public void loadFromNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
|
|
||||||
// Check if url is empty.
|
|
||||||
if (TextUtils.isEmpty(thumbnailUrl))
|
|
||||||
return;
|
|
||||||
|
|
||||||
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
|
|
||||||
Glide.with(context)
|
|
||||||
.load(url)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
|
|
||||||
.centerCrop()
|
|
||||||
.into(imageView);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
106
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to create cover cache.
|
||||||
|
* It is used to store the covers of the library.
|
||||||
|
* Makes use of Glide (which can avoid repeating requests) to download covers.
|
||||||
|
* Names of files are created with the md5 of the thumbnail URL.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @constructor creates an instance of the cover cache.
|
||||||
|
*/
|
||||||
|
class CoverCache(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val COVERS_DIR = "covers"
|
||||||
|
private const val CUSTOM_COVERS_DIR = "covers/custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache directory used for cache management.
|
||||||
|
*/
|
||||||
|
private val cacheDir = getCacheDir(COVERS_DIR)
|
||||||
|
|
||||||
|
private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cover from cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return cover image.
|
||||||
|
*/
|
||||||
|
fun getCoverFile(manga: Manga): File? {
|
||||||
|
return manga.thumbnail_url?.let {
|
||||||
|
File(cacheDir, DiskUtil.hashKeyForDisk(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the custom cover from cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return cover image.
|
||||||
|
*/
|
||||||
|
fun getCustomCoverFile(manga: Manga): File {
|
||||||
|
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given stream as the manga's custom cover to cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @param inputStream the stream to copy.
|
||||||
|
* @throws IOException if there's any error.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
|
||||||
|
getCustomCoverFile(manga).outputStream().use {
|
||||||
|
inputStream.copyTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the cover files of the manga from the cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @param deleteCustomCover whether the custom cover should be deleted.
|
||||||
|
* @return number of files that were deleted.
|
||||||
|
*/
|
||||||
|
fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int {
|
||||||
|
var deleted = 0
|
||||||
|
|
||||||
|
getCoverFile(manga)?.let {
|
||||||
|
if (it.exists() && it.delete()) ++deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteCustomCover) {
|
||||||
|
if (deleteCustomCover(manga)) ++deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete custom cover of the manga from the cache
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return whether the cover was deleted.
|
||||||
|
*/
|
||||||
|
fun deleteCustomCover(manga: Manga): Boolean {
|
||||||
|
return getCustomCoverFile(manga).let {
|
||||||
|
it.exists() && it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheDir(dir: String): File {
|
||||||
|
return context.getExternalFilesDir(dir)
|
||||||
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.bumptech.glide.GlideBuilder;
|
|
||||||
import com.bumptech.glide.load.DecodeFormat;
|
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
|
||||||
import com.bumptech.glide.module.GlideModule;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to update Glide module settings
|
|
||||||
*/
|
|
||||||
public class CoverGlideModule implements GlideModule {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void applyOptions(Context context, GlideBuilder builder) {
|
|
||||||
// Bitmaps decoded from most image formats (other than GIFs with hidden configs)
|
|
||||||
// will be decoded with the ARGB_8888 config.
|
|
||||||
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
|
|
||||||
|
|
||||||
// Set the cache size of Glide to 15 MiB
|
|
||||||
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerComponents(Context context, Glide glide) {
|
|
||||||
// Nothing to see here!
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,420 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.Queries;
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite;
|
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteCollectionOfObjects;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteObject;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects;
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject;
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery;
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query;
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterSQLiteTypeMapping;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
|
||||||
import eu.kanade.tachiyomi.util.ChapterRecognition;
|
|
||||||
import rx.Observable;
|
|
||||||
|
|
||||||
public class DatabaseHelper {
|
|
||||||
|
|
||||||
private StorIOSQLite db;
|
|
||||||
|
|
||||||
public DatabaseHelper(Context context) {
|
|
||||||
|
|
||||||
db = DefaultStorIOSQLite.builder()
|
|
||||||
.sqliteOpenHelper(new DbOpenHelper(context))
|
|
||||||
.addTypeMapping(Manga.class, new MangaSQLiteTypeMapping())
|
|
||||||
.addTypeMapping(Chapter.class, new ChapterSQLiteTypeMapping())
|
|
||||||
.addTypeMapping(MangaSync.class, new MangaSyncSQLiteTypeMapping())
|
|
||||||
.addTypeMapping(Category.class, new CategorySQLiteTypeMapping())
|
|
||||||
.addTypeMapping(MangaCategory.class, new MangaCategorySQLiteTypeMapping())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mangas related queries
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Manga> getMangas() {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(Manga.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Manga> getLibraryMangas() {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(Manga.class)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(LibraryMangaGetResolver.QUERY)
|
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Manga> getFavoriteMangas() {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(Manga.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where(MangaTable.COLUMN_FAVORITE + "=?")
|
|
||||||
.whereArgs(1)
|
|
||||||
.orderBy(MangaTable.COLUMN_TITLE)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetObject<Manga> getManga(String url, int sourceId) {
|
|
||||||
return db.get()
|
|
||||||
.object(Manga.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where(MangaTable.COLUMN_URL + "=? AND " + MangaTable.COLUMN_SOURCE + "=?")
|
|
||||||
.whereArgs(url, sourceId)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetObject<Manga> getManga(long id) {
|
|
||||||
return db.get()
|
|
||||||
.object(Manga.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where(MangaTable.COLUMN_ID + "=?")
|
|
||||||
.whereArgs(id)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutObject<Manga> insertManga(Manga manga) {
|
|
||||||
return db.put()
|
|
||||||
.object(manga)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutCollectionOfObjects<Manga> insertMangas(List<Manga> mangas) {
|
|
||||||
return db.put()
|
|
||||||
.objects(mangas)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteObject<Manga> deleteManga(Manga manga) {
|
|
||||||
return db.delete()
|
|
||||||
.object(manga)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteCollectionOfObjects<Manga> deleteMangas(List<Manga> mangas) {
|
|
||||||
return db.delete()
|
|
||||||
.objects(mangas)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteByQuery deleteMangasNotInLibrary() {
|
|
||||||
return db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where(MangaTable.COLUMN_FAVORITE + "=?")
|
|
||||||
.whereArgs(0)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Chapters related queries
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Chapter> getChapters(Manga manga) {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(Chapter.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where(ChapterTable.COLUMN_MANGA_ID + "=?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(MangaChapter.class)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(MangaChapterGetResolver.getRecentChaptersQuery(date))
|
|
||||||
.observesTables(ChapterTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetObject<Chapter> getNextChapter(Chapter chapter) {
|
|
||||||
// Add a delta to the chapter number, because binary decimal representation
|
|
||||||
// can retrieve the same chapter again
|
|
||||||
double chapterNumber = chapter.chapter_number + 0.00001;
|
|
||||||
|
|
||||||
return db.get()
|
|
||||||
.object(Chapter.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
|
||||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">? AND " +
|
|
||||||
ChapterTable.COLUMN_CHAPTER_NUMBER + "<=?")
|
|
||||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
|
||||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetObject<Chapter> getPreviousChapter(Chapter chapter) {
|
|
||||||
// Add a delta to the chapter number, because binary decimal representation
|
|
||||||
// can retrieve the same chapter again
|
|
||||||
double chapterNumber = chapter.chapter_number - 0.00001;
|
|
||||||
|
|
||||||
return db.get()
|
|
||||||
.object(Chapter.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
|
||||||
ChapterTable.COLUMN_CHAPTER_NUMBER + "<? AND " +
|
|
||||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
|
|
||||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
|
||||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetObject<Chapter> getNextUnreadChapter(Manga manga) {
|
|
||||||
return db.get()
|
|
||||||
.object(Chapter.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
|
|
||||||
ChapterTable.COLUMN_READ + "=? AND " +
|
|
||||||
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
|
|
||||||
.whereArgs(manga.id, 0, 0)
|
|
||||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutObject<Chapter> insertChapter(Chapter chapter) {
|
|
||||||
return db.put()
|
|
||||||
.object(chapter)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutCollectionOfObjects<Chapter> insertChapters(List<Chapter> chapters) {
|
|
||||||
return db.put()
|
|
||||||
.objects(chapters)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new chapters or delete if the source deletes them
|
|
||||||
public Observable<Pair<Integer, Integer>> insertOrRemoveChapters(Manga manga, List<Chapter> sourceChapters) {
|
|
||||||
List<Chapter> dbChapters = getChapters(manga).executeAsBlocking();
|
|
||||||
|
|
||||||
Observable<List<Chapter>> newChapters = Observable.from(sourceChapters)
|
|
||||||
.filter(c -> !dbChapters.contains(c))
|
|
||||||
.doOnNext(c -> {
|
|
||||||
c.manga_id = manga.id;
|
|
||||||
ChapterRecognition.parseChapterNumber(c, manga);
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
Observable<List<Chapter>> deletedChapters = Observable.from(dbChapters)
|
|
||||||
.filter(c -> !sourceChapters.contains(c))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Observable.zip(newChapters, deletedChapters, (toAdd, toDelete) -> {
|
|
||||||
int added = 0;
|
|
||||||
int deleted = 0;
|
|
||||||
db.internal().beginTransaction();
|
|
||||||
try {
|
|
||||||
TreeSet<Float> deletedReadChapterNumbers = new TreeSet<>();
|
|
||||||
if (!toDelete.isEmpty()) {
|
|
||||||
for (Chapter c : toDelete) {
|
|
||||||
if (c.read) {
|
|
||||||
deletedReadChapterNumbers.add(c.chapter_number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toAdd.isEmpty()) {
|
|
||||||
// Set the date fetch for new items in reverse order to allow another sorting method.
|
|
||||||
// Sources MUST return the chapters from most to less recent, which is common.
|
|
||||||
long now = new Date().getTime();
|
|
||||||
|
|
||||||
for (int i = toAdd.size() - 1; i >= 0; i--) {
|
|
||||||
Chapter c = toAdd.get(i);
|
|
||||||
c.date_fetch = now++;
|
|
||||||
// Try to mark already read chapters as read when the source deletes them
|
|
||||||
if (c.chapter_number != -1 && deletedReadChapterNumbers.contains(c.chapter_number)) {
|
|
||||||
c.read = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
|
|
||||||
}
|
|
||||||
|
|
||||||
db.internal().setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
db.internal().endTransaction();
|
|
||||||
}
|
|
||||||
return Pair.create(added, deleted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteObject<Chapter> deleteChapter(Chapter chapter) {
|
|
||||||
return db.delete()
|
|
||||||
.object(chapter)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteCollectionOfObjects<Chapter> deleteChapters(List<Chapter> chapters) {
|
|
||||||
return db.delete()
|
|
||||||
.objects(chapters)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manga sync related queries
|
|
||||||
|
|
||||||
public PreparedGetObject<MangaSync> getMangaSync(Manga manga, MangaSyncService sync) {
|
|
||||||
return db.get()
|
|
||||||
.object(MangaSync.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where(MangaSyncTable.COLUMN_MANGA_ID + "=? AND " +
|
|
||||||
MangaSyncTable.COLUMN_SYNC_ID + "=?")
|
|
||||||
.whereArgs(manga.id, sync.getId())
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<MangaSync> getMangasSync(Manga manga) {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(MangaSync.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where(MangaSyncTable.COLUMN_MANGA_ID + "=?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutObject<MangaSync> insertMangaSync(MangaSync manga) {
|
|
||||||
return db.put()
|
|
||||||
.object(manga)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteObject<MangaSync> deleteMangaSync(MangaSync manga) {
|
|
||||||
return db.delete()
|
|
||||||
.object(manga)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories related queries
|
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Category> getCategories() {
|
|
||||||
return db.get()
|
|
||||||
.listOfObjects(Category.class)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(CategoryTable.TABLE)
|
|
||||||
.orderBy(CategoryTable.COLUMN_ORDER)
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutObject<Category> insertCategory(Category category) {
|
|
||||||
return db.put()
|
|
||||||
.object(category)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
|
|
||||||
return db.put()
|
|
||||||
.objects(categories)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteObject<Category> deleteCategory(Category category) {
|
|
||||||
return db.delete()
|
|
||||||
.object(category)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteCollectionOfObjects<Category> deleteCategories(List<Category> categories) {
|
|
||||||
return db.delete()
|
|
||||||
.objects(categories)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutObject<MangaCategory> insertMangaCategory(MangaCategory mangaCategory) {
|
|
||||||
return db.put()
|
|
||||||
.object(mangaCategory)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedPutCollectionOfObjects<MangaCategory> insertMangasCategories(List<MangaCategory> mangasCategories) {
|
|
||||||
return db.put()
|
|
||||||
.objects(mangasCategories)
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PreparedDeleteByQuery deleteOldMangasCategories(List<Manga> mangas) {
|
|
||||||
List<Long> mangaIds = Observable.from(mangas)
|
|
||||||
.map(manga -> manga.id)
|
|
||||||
.toList().toBlocking().single();
|
|
||||||
|
|
||||||
return db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaCategoryTable.TABLE)
|
|
||||||
.where(MangaCategoryTable.COLUMN_MANGA_ID + " IN ("
|
|
||||||
+ Queries.placeholders(mangas.size()) + ")")
|
|
||||||
.whereArgs(mangaIds.toArray())
|
|
||||||
.build())
|
|
||||||
.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMangaCategories(List<MangaCategory> mangasCategories, List<Manga> mangas) {
|
|
||||||
db.internal().beginTransaction();
|
|
||||||
try {
|
|
||||||
deleteOldMangasCategories(mangas).executeAsBlocking();
|
|
||||||
insertMangasCategories(mangasCategories).executeAsBlocking();
|
|
||||||
db.internal().setTransactionSuccessful();
|
|
||||||
} finally {
|
|
||||||
db.internal().endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
|
||||||
|
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.queries.CategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides operations to manage the database through its interfaces.
|
||||||
|
*/
|
||||||
|
open class DatabaseHelper(context: Context) :
|
||||||
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
|
.callback(DbOpenCallback())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
|
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
|
||||||
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
|
||||||
|
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
||||||
|
lowLevel().beginTransaction()
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
lowLevel().setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
lowLevel().endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||||
|
lowLevel().beginTransaction()
|
||||||
|
try {
|
||||||
|
val result = block()
|
||||||
|
lowLevel().setTransactionSuccessful()
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
lowLevel().endTransaction()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
|
||||||
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Name of the database file.
|
||||||
|
*/
|
||||||
|
const val DATABASE_NAME = "tachiyomi.db"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of the database.
|
||||||
|
*/
|
||||||
|
const val DATABASE_VERSION = 11
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
|
execSQL(MangaTable.createTableQuery)
|
||||||
|
execSQL(ChapterTable.createTableQuery)
|
||||||
|
execSQL(TrackTable.createTableQuery)
|
||||||
|
execSQL(CategoryTable.createTableQuery)
|
||||||
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
|
||||||
|
// DB indexes
|
||||||
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
|
execSQL(MangaTable.createLibraryIndexQuery)
|
||||||
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
|
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
||||||
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if (oldVersion < 11) {
|
||||||
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
|
||||||
|
|
||||||
public class DbOpenHelper extends SQLiteOpenHelper {
|
|
||||||
|
|
||||||
public static final String DATABASE_NAME = "tachiyomi.db";
|
|
||||||
public static final int DATABASE_VERSION = 1;
|
|
||||||
|
|
||||||
public DbOpenHelper(@NonNull Context context) {
|
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@NonNull SQLiteDatabase db) {
|
|
||||||
db.execSQL(MangaTable.getCreateTableQuery());
|
|
||||||
db.execSQL(ChapterTable.getCreateTableQuery());
|
|
||||||
db.execSQL(MangaSyncTable.getCreateTableQuery());
|
|
||||||
db.execSQL(CategoryTable.getCreateTableQuery());
|
|
||||||
db.execSQL(MangaCategoryTable.getCreateTableQuery());
|
|
||||||
|
|
||||||
// DB indexes
|
|
||||||
db.execSQL(MangaTable.getCreateUrlIndexQuery());
|
|
||||||
db.execSQL(MangaTable.getCreateFavoriteIndexQuery());
|
|
||||||
db.execSQL(ChapterTable.getCreateMangaIdIndexQuery());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConfigure(@NonNull SQLiteDatabase db) {
|
|
||||||
db.setForeignKeyConstraintsEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
|
|
||||||
|
interface DbProvider {
|
||||||
|
|
||||||
|
val db: DefaultStorIOSQLite
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
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_ID
|
||||||
|
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.TABLE
|
||||||
|
|
||||||
|
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||||
|
CategoryPutResolver(),
|
||||||
|
CategoryGetResolver(),
|
||||||
|
CategoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_NAME, obj.name)
|
||||||
|
put(COL_ORDER, obj.order)
|
||||||
|
put(COL_FLAGS, obj.flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
|
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
||||||
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||||
|
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_BOOKMARK
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
|
|
||||||
|
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||||
|
ChapterPutResolver(),
|
||||||
|
ChapterGetResolver(),
|
||||||
|
ChapterDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
|
put(COL_URL, obj.url)
|
||||||
|
put(COL_NAME, obj.name)
|
||||||
|
put(COL_READ, obj.read)
|
||||||
|
put(COL_SCANLATOR, obj.scanlator)
|
||||||
|
put(COL_BOOKMARK, obj.bookmark)
|
||||||
|
put(COL_DATE_FETCH, obj.date_fetch)
|
||||||
|
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||||
|
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||||
|
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||||
|
put(COL_SOURCE_ORDER, obj.source_order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
||||||
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
|
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
||||||
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
|
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||||
|
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||||
|
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||||
|
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||||
|
|
||||||
|
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||||
|
HistoryPutResolver(),
|
||||||
|
HistoryGetResolver(),
|
||||||
|
HistoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_CHAPTER_ID, obj.chapter_id)
|
||||||
|
put(COL_LAST_READ, obj.last_read)
|
||||||
|
put(COL_TIME_READ, obj.time_read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||||
|
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||||
|
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||||
|
|
||||||
|
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||||
|
MangaCategoryPutResolver(),
|
||||||
|
MangaCategoryGetResolver(),
|
||||||
|
MangaCategoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
|
put(COL_CATEGORY_ID, obj.category_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
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_CHAPTER_FLAGS
|
||||||
|
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_FAVORITE
|
||||||
|
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_INITIALIZED
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
|
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_THUMBNAIL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||||
|
|
||||||
|
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||||
|
MangaPutResolver(),
|
||||||
|
MangaGetResolver(),
|
||||||
|
MangaDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_SOURCE, obj.source)
|
||||||
|
put(COL_URL, obj.url)
|
||||||
|
put(COL_ARTIST, obj.artist)
|
||||||
|
put(COL_AUTHOR, obj.author)
|
||||||
|
put(COL_DESCRIPTION, obj.description)
|
||||||
|
put(COL_GENRE, obj.genre)
|
||||||
|
put(COL_TITLE, obj.title)
|
||||||
|
put(COL_STATUS, obj.status)
|
||||||
|
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||||
|
put(COL_FAVORITE, obj.favorite)
|
||||||
|
put(COL_LAST_UPDATE, obj.last_update)
|
||||||
|
put(COL_INITIALIZED, obj.initialized)
|
||||||
|
put(COL_VIEWER, obj.viewer)
|
||||||
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
|
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||||
|
put(COL_DATE_ADDED, obj.date_added)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseMangaGetResolver {
|
||||||
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||||
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
|
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||||
|
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||||
|
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
||||||
|
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
||||||
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||||
|
return mapBaseFromCursor(MangaImpl(), cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
|
||||||
|
|
||||||
|
class TrackTypeMapping : SQLiteTypeMapping<Track>(
|
||||||
|
TrackPutResolver(),
|
||||||
|
TrackGetResolver(),
|
||||||
|
TrackDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
|
put(COL_SYNC_ID, obj.sync_id)
|
||||||
|
put(COL_MEDIA_ID, obj.media_id)
|
||||||
|
put(COL_LIBRARY_ID, obj.library_id)
|
||||||
|
put(COL_TITLE, obj.title)
|
||||||
|
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||||
|
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||||
|
put(COL_STATUS, obj.status)
|
||||||
|
put(COL_TRACKING_URL, obj.tracking_url)
|
||||||
|
put(COL_SCORE, obj.score)
|
||||||
|
put(COL_START_DATE, obj.started_reading_date)
|
||||||
|
put(COL_FINISH_DATE, obj.finished_reading_date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
|
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
||||||
|
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
||||||
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
|
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
|
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
||||||
|
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
||||||
|
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
|
||||||
public class Category implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
|
|
||||||
public Integer id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
|
|
||||||
public int order;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
|
|
||||||
public int flags;
|
|
||||||
|
|
||||||
public Category() {}
|
|
||||||
|
|
||||||
public static Category create(String name) {
|
|
||||||
Category c = new Category();
|
|
||||||
c.name = name;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Category createDefault() {
|
|
||||||
Category c = create("Default");
|
|
||||||
c.id = 0;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Category : Serializable {
|
||||||
|
|
||||||
|
var id: Int?
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var order: Int
|
||||||
|
|
||||||
|
var flags: Int
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
this.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class CategoryImpl : Category {
|
||||||
|
|
||||||
|
override var id: Int? = null
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var order: Int = 0
|
||||||
|
|
||||||
|
override var flags: Int = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val category = other as Category
|
||||||
|
return name == category.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
|
||||||
public class Chapter implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
|
|
||||||
public Long manga_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
|
|
||||||
public String url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
|
|
||||||
public boolean read;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
|
|
||||||
public int last_page_read;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
|
|
||||||
public long date_fetch;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
|
|
||||||
public long date_upload;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
|
|
||||||
public float chapter_number;
|
|
||||||
|
|
||||||
public int status;
|
|
||||||
|
|
||||||
private transient List<Page> pages;
|
|
||||||
|
|
||||||
public Chapter() {}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = UrlUtil.getPath(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Chapter chapter = (Chapter) o;
|
|
||||||
|
|
||||||
return url.equals(chapter.url);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return url.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Chapter create() {
|
|
||||||
Chapter chapter = new Chapter();
|
|
||||||
chapter.chapter_number = -1;
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Page> getPages() {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPages(List<Page> pages) {
|
|
||||||
this.pages = pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDownloaded() {
|
|
||||||
return status == Download.DOWNLOADED;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Chapter : SChapter, Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var manga_id: Long?
|
||||||
|
|
||||||
|
var read: Boolean
|
||||||
|
|
||||||
|
var bookmark: Boolean
|
||||||
|
|
||||||
|
var last_page_read: Int
|
||||||
|
|
||||||
|
var date_fetch: Long
|
||||||
|
|
||||||
|
var source_order: Int
|
||||||
|
|
||||||
|
val isRecognizedNumber: Boolean
|
||||||
|
get() = chapter_number >= 0f
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(): Chapter = ChapterImpl().apply {
|
||||||
|
chapter_number = -1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class ChapterImpl : Chapter {
|
||||||
|
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
override var manga_id: Long? = null
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
|
||||||
|
override var read: Boolean = false
|
||||||
|
|
||||||
|
override var bookmark: Boolean = false
|
||||||
|
|
||||||
|
override var last_page_read: Int = 0
|
||||||
|
|
||||||
|
override var date_fetch: Long = 0
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
|
override var chapter_number: Float = 0f
|
||||||
|
|
||||||
|
override var source_order: Int = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val chapter = other as Chapter
|
||||||
|
if (url != chapter.url) return false
|
||||||
|
return id == chapter.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return url.hashCode() + id.hashCode()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
interface History : Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
var chapter_id: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
var last_read: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
var time_read: Long
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History constructor
|
||||||
|
*
|
||||||
|
* @param chapter chapter object
|
||||||
|
* @return history object
|
||||||
|
*/
|
||||||
|
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||||
|
this.chapter_id = chapter.id!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
class HistoryImpl : History {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
override var chapter_id: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
override var last_read: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
override var time_read: Long = 0
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class LibraryManga : MangaImpl() {
|
||||||
|
|
||||||
|
var unread: Int = 0
|
||||||
|
|
||||||
|
var category: Int = 0
|
||||||
|
}
|
@ -1,194 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaTable.TABLE)
|
|
||||||
public class Manga implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
|
|
||||||
public int source;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
|
|
||||||
public String url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
|
|
||||||
public String artist;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
|
|
||||||
public String author;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
|
|
||||||
public String description;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
|
|
||||||
public String genre;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
|
|
||||||
public String title;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
|
|
||||||
public int status;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
|
|
||||||
public String thumbnail_url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
|
|
||||||
public boolean favorite;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
|
|
||||||
public long last_update;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
|
|
||||||
public boolean initialized;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
|
|
||||||
public int viewer;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
|
|
||||||
public int chapter_flags;
|
|
||||||
|
|
||||||
public int unread;
|
|
||||||
|
|
||||||
public int category;
|
|
||||||
|
|
||||||
public static final int UNKNOWN = 0;
|
|
||||||
public static final int ONGOING = 1;
|
|
||||||
public static final int COMPLETED = 2;
|
|
||||||
public static final int LICENSED = 3;
|
|
||||||
|
|
||||||
public static final int SORT_AZ = 0x00000000;
|
|
||||||
public static final int SORT_ZA = 0x00000001;
|
|
||||||
public static final int SORT_MASK = 0x00000001;
|
|
||||||
|
|
||||||
public static final int SHOW_UNREAD = 0x00000002;
|
|
||||||
public static final int SHOW_READ = 0x00000004;
|
|
||||||
public static final int READ_MASK = 0x00000006;
|
|
||||||
|
|
||||||
public static final int SHOW_DOWNLOADED = 0x00000008;
|
|
||||||
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
|
||||||
public static final int DOWNLOADED_MASK = 0x00000018;
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
|
||||||
public static final int SHOW_ALL = 0x00000000;
|
|
||||||
|
|
||||||
public static final int DISPLAY_NAME = 0x00000000;
|
|
||||||
public static final int DISPLAY_NUMBER = 0x00100000;
|
|
||||||
public static final int DISPLAY_MASK = 0x00100000;
|
|
||||||
|
|
||||||
public Manga() {}
|
|
||||||
|
|
||||||
public static Manga create(String pathUrl) {
|
|
||||||
Manga m = new Manga();
|
|
||||||
m.url = pathUrl;
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = UrlUtil.getPath(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyFrom(Manga other) {
|
|
||||||
if (other.title != null)
|
|
||||||
title = other.title;
|
|
||||||
|
|
||||||
if (other.author != null)
|
|
||||||
author = other.author;
|
|
||||||
|
|
||||||
if (other.artist != null)
|
|
||||||
artist = other.artist;
|
|
||||||
|
|
||||||
if (other.url != null)
|
|
||||||
url = other.url;
|
|
||||||
|
|
||||||
if (other.description != null)
|
|
||||||
description = other.description;
|
|
||||||
|
|
||||||
if (other.genre != null)
|
|
||||||
genre = other.genre;
|
|
||||||
|
|
||||||
if (other.thumbnail_url != null)
|
|
||||||
thumbnail_url = other.thumbnail_url;
|
|
||||||
|
|
||||||
status = other.status;
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus(Context context) {
|
|
||||||
switch (status) {
|
|
||||||
case ONGOING:
|
|
||||||
return context.getString(R.string.ongoing);
|
|
||||||
case COMPLETED:
|
|
||||||
return context.getString(R.string.completed);
|
|
||||||
case LICENSED:
|
|
||||||
return context.getString(R.string.licensed);
|
|
||||||
default:
|
|
||||||
return context.getString(R.string.unknown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChapterOrder(int order) {
|
|
||||||
setFlags(order, SORT_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDisplayMode(int mode) {
|
|
||||||
setFlags(mode, DISPLAY_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReadFilter(int filter) {
|
|
||||||
setFlags(filter, READ_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadedFilter(int filter) {
|
|
||||||
setFlags(filter, DOWNLOADED_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFlags(int flag, int mask) {
|
|
||||||
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean sortChaptersAZ() {
|
|
||||||
return (chapter_flags & SORT_MASK) == SORT_AZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
|
||||||
public int getDisplayMode() {
|
|
||||||
return chapter_flags & DISPLAY_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getReadFilter() {
|
|
||||||
return chapter_flags & READ_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDownloadedFilter() {
|
|
||||||
return chapter_flags & DOWNLOADED_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Manga manga = (Manga) o;
|
|
||||||
|
|
||||||
return url.equals(manga.url);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return url.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,114 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
|
interface Manga : SManga {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var source: Long
|
||||||
|
|
||||||
|
var favorite: Boolean
|
||||||
|
|
||||||
|
var last_update: Long
|
||||||
|
|
||||||
|
var date_added: Long
|
||||||
|
|
||||||
|
var viewer: Int
|
||||||
|
|
||||||
|
var chapter_flags: Int
|
||||||
|
|
||||||
|
var cover_last_modified: Long
|
||||||
|
|
||||||
|
fun setChapterOrder(order: Int) {
|
||||||
|
setFlags(order, SORT_MASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortDescending(): Boolean {
|
||||||
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getGenres(): List<String>? {
|
||||||
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to display the chapter's title one way or another
|
||||||
|
var displayMode: Int
|
||||||
|
get() = chapter_flags and DISPLAY_MASK
|
||||||
|
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||||
|
|
||||||
|
var readFilter: Int
|
||||||
|
get() = chapter_flags and READ_MASK
|
||||||
|
set(filter) = setFlags(filter, READ_MASK)
|
||||||
|
|
||||||
|
var downloadedFilter: Int
|
||||||
|
get() = chapter_flags and DOWNLOADED_MASK
|
||||||
|
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||||
|
|
||||||
|
var bookmarkedFilter: Int
|
||||||
|
get() = chapter_flags and BOOKMARKED_MASK
|
||||||
|
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
||||||
|
|
||||||
|
var sorting: Int
|
||||||
|
get() = chapter_flags and SORTING_MASK
|
||||||
|
set(sort) = setFlags(sort, SORTING_MASK)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val SORT_DESC = 0x00000000
|
||||||
|
const val SORT_ASC = 0x00000001
|
||||||
|
const val SORT_MASK = 0x00000001
|
||||||
|
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
|
const val SHOW_UNREAD = 0x00000002
|
||||||
|
const val SHOW_READ = 0x00000004
|
||||||
|
const val READ_MASK = 0x00000006
|
||||||
|
|
||||||
|
const val SHOW_DOWNLOADED = 0x00000008
|
||||||
|
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
|
const val DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
|
const val SHOW_BOOKMARKED = 0x00000020
|
||||||
|
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
||||||
|
const val BOOKMARKED_MASK = 0x00000060
|
||||||
|
|
||||||
|
const val SORTING_SOURCE = 0x00000000
|
||||||
|
const val SORTING_NUMBER = 0x00000100
|
||||||
|
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
|
const val DISPLAY_NAME = 0x00000000
|
||||||
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
|
const val DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
||||||
|
url = pathUrl
|
||||||
|
this.title = title
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.toMangaInfo(): MangaInfo {
|
||||||
|
return MangaInfo(
|
||||||
|
artist = this.artist ?: "",
|
||||||
|
author = this.author ?: "",
|
||||||
|
cover = this.thumbnail_url ?: "",
|
||||||
|
description = this.description ?: "",
|
||||||
|
genres = this.getGenres() ?: emptyList(),
|
||||||
|
key = this.url,
|
||||||
|
status = this.status,
|
||||||
|
title = this.title
|
||||||
|
)
|
||||||
|
}
|