Compare commits
1829 Commits
Author | SHA1 | Date | |
---|---|---|---|
6aff438a16 | |||
13324dd1a1 | |||
ae9bf06b46 | |||
5236834911 | |||
bf80dd622c | |||
662b71436e | |||
f608cb55eb | |||
6ba82da029 | |||
f407e30b6e | |||
4e7b8c98f9 | |||
5f9574541f | |||
08a6db7d6e | |||
b485e1d657 | |||
e8d8621f06 | |||
4cefbce7c3 | |||
fa31369f99 | |||
d0bf93ebb7 | |||
41a747c7e7 | |||
8882cd4787 | |||
6676490e09 | |||
68bea8a196 | |||
25995c09a0 | |||
0eb8d7d081 | |||
554f890ae3 | |||
dd1743698f | |||
b092e98ac9 | |||
9ee6262aed | |||
24a2d86f41 | |||
b5c5c66336 | |||
7654feb6a8 | |||
a598ac3993 | |||
cab919d74c | |||
60a929b92c | |||
356b7c346a | |||
ad57fde1c5 | |||
17f7dea21b | |||
b40af7c3c6 | |||
9065362fde | |||
d264b03ca1 | |||
ad9bad3d17 | |||
dfd858034f | |||
58ad8fa8c0 | |||
38610d8a24 | |||
27cec697bf | |||
024f9a8c76 | |||
f7cc36f2f0 | |||
ef5148ebb4 | |||
6dbc0a6fd5 | |||
fba3f9d501 | |||
d9f8137362 | |||
28416489b2 | |||
54a23ddd1f | |||
3287ca9cf2 | |||
a59e134862 | |||
1f8c5b0120 | |||
c7f839ea4a | |||
d981245723 | |||
1f729f1cb3 | |||
b4577d6676 | |||
544adb9940 | |||
1875c4a752 | |||
5f0493f1e5 | |||
c749e50bec | |||
a4e5e3ece5 | |||
2a69d1b051 | |||
126e1e2d9d | |||
0586e1d3ad | |||
07cb1c237e | |||
f4f1efe5fa | |||
37fdf4d434 | |||
99b46096a4 | |||
12e90ae35e | |||
023311a874 | |||
155a4dd463 | |||
15bed1ac4c | |||
27f55f8098 | |||
00598879e2 | |||
df274a0a78 | |||
0dc4862d79 | |||
a3f1b72126 | |||
5ff10799e4 | |||
a82e5f5452 | |||
e10cb0e632 | |||
c7e07a6df0 | |||
2e0c778090 | |||
592050c668 | |||
02c9191525 | |||
d421401626 | |||
b2d4e5ab84 | |||
84e023607c | |||
f145fd0dec | |||
42a9f911d8 | |||
9567d55312 | |||
531cd99247 | |||
f3660d88dd | |||
3accb9a08b | |||
63ce7371bb | |||
01c3498dbf | |||
b3471234ad | |||
b2d697131c | |||
ef49fc91d8 | |||
6222b47a4f | |||
f58e3c390a | |||
7504621a24 | |||
88e49a9b8b | |||
5b23f29d06 | |||
c1bdebee78 | |||
ddd4cc10ff | |||
0ca62a4acc | |||
4f1275ac01 | |||
b2fee7035f | |||
e15d7cb548 | |||
3257cbe21f | |||
1237af1ff3 | |||
68600b337e | |||
dac2072eaa | |||
1b921f9845 | |||
a3992d9fbe | |||
efd2a0cb7b | |||
fba428257b | |||
ff36901007 | |||
940d8389b5 | |||
f7a6cbe5e2 | |||
7aa379a857 | |||
443024cebb | |||
1657f04d55 | |||
407e798fdb | |||
4054f2a6a0 | |||
468cdf603c | |||
988ec6a224 | |||
bdbdf211e2 | |||
0437703cbf | |||
71aa592111 | |||
d501c02f8b | |||
9daf0e78b8 | |||
dfa07a5f35 | |||
437c995d12 | |||
cc6ae9d1a8 | |||
c58e4f4dee | |||
c87b0e77de | |||
355d5af8ae | |||
3d99a8ebdb | |||
c4b975b777 | |||
2911fe7a1a | |||
14c114756d | |||
e7a8107279 | |||
bff73b1b40 | |||
c255f57d95 | |||
64c47bbaed | |||
e0b7698d40 | |||
a01792ac9a | |||
3ba078f64c | |||
a16240f123 | |||
e5a120e778 | |||
2ba60e9114 | |||
472ce5a5e4 | |||
99ba84c810 | |||
78285bdf37 | |||
5a7f2684b3 | |||
d912a42249 | |||
6d8c4fb8b1 | |||
a63cecbfcb | |||
4a5bceb4e4 | |||
86541445b7 | |||
4e826aa8e7 | |||
b6e6f490e9 | |||
2145e878a4 | |||
355f6db255 | |||
bc7632bf02 | |||
609d8c9685 | |||
2f08515455 | |||
7f450e185d | |||
747879b4ec | |||
4193870fa6 | |||
cdc5de3f1b | |||
bc34d4fa88 | |||
6fd4af8736 | |||
b5c2934270 | |||
94f5117941 | |||
112e233498 | |||
18b1326f3a | |||
1e58b05ead | |||
938919bd9b | |||
b6b78994d8 | |||
fddd8ce305 | |||
ccff337975 | |||
fde6b7af4f | |||
0657db7dcb | |||
d1c2eaf6d5 | |||
91bb6b9016 | |||
90351c6e9e | |||
dd4740e54f | |||
48e7cbd76c | |||
f51e32f39b | |||
ae42f59102 | |||
5c8006f9b7 | |||
aa5861d3ca | |||
7a64bf55cb | |||
d4c9ab793f | |||
48d2849d97 | |||
776610d0e6 | |||
3a790f3d66 | |||
7382042288 | |||
33992d80bf | |||
a92b0e567b | |||
829a65e515 | |||
03ad48c055 | |||
89837e4ced | |||
ace1db21d1 | |||
8bb69c455b | |||
2dae706198 | |||
3eda2a220a | |||
61e5440b7c | |||
2e2663bad9 | |||
f4dd150b70 | |||
2b35d22e25 | |||
f590378761 | |||
f5f592be91 | |||
7a373fb43a | |||
aded11e599 | |||
41d7cee020 | |||
f2ef6a20e6 | |||
a398c3fb81 | |||
2a454b44cc | |||
7b66ece895 | |||
b5017eebbf | |||
aa67229daf | |||
5af68186d6 | |||
545bc0e605 | |||
291168f4de | |||
9facb51f22 | |||
5b7d8c5e37 | |||
5945937e4b | |||
9f9f9872eb | |||
3566072f4a | |||
b85cd86b24 | |||
79c3767fff | |||
cf1609a429 | |||
3aeac7e7b5 | |||
1557f713f4 | |||
b63d24ac1a | |||
348c1ff29d | |||
717e55497f | |||
d84b5e8b46 | |||
5f9ddf9ff5 | |||
bbee093c63 | |||
e8c35ae4e1 | |||
1607658c30 | |||
2e9ef373f3 | |||
ec6eef6d37 | |||
45a19d15ec | |||
7191552126 | |||
cfa07490e5 | |||
ae40990eb9 | |||
9f2fe33ce0 | |||
33660de6b1 | |||
13d25e0849 | |||
6662e2002f | |||
d4081dc899 | |||
62dffb8226 | |||
cb6aa18480 | |||
d5cfbef42b | |||
535abcbb8b | |||
c34b548a3e | |||
9bf452856c | |||
17109ab760 | |||
6bc6e1a1d1 | |||
7eef4f7fbf | |||
75bec6a8e3 | |||
0a10f66053 | |||
58860b51a2 | |||
3ee652b61a | |||
426ed7308b | |||
0ecfef3f70 | |||
5f7e34b6a1 | |||
34cb24fe34 | |||
1490112135 | |||
c4716a3f4c | |||
0a54901eb0 | |||
fea2e0a265 | |||
d3c087375b | |||
a93c0577ac | |||
e4dc35674d | |||
8a668ba7b9 | |||
ee9a68b040 | |||
78e8d40649 | |||
660e13b701 | |||
0685382083 | |||
04a993c997 | |||
7cae3095c4 | |||
e288bf902b | |||
a083e1f71a | |||
86b9d7e843 | |||
628bd5d6b4 | |||
00285a782c | |||
16be469ecb | |||
fdcbc4cffa | |||
fc548304cf | |||
7c7ff8165e | |||
496a476c13 | |||
441fc6e45b | |||
cf7ec6aa76 | |||
db2dd4b6c6 | |||
a68417a0b0 | |||
2a5102a457 | |||
837d8f5f30 | |||
1a5858e99b | |||
4044427d93 | |||
f667f85fa5 | |||
5cddc0c387 | |||
cbc01dd6f1 | |||
b820c7debf | |||
2bee072cba | |||
80710b0b94 | |||
3319ccfd41 | |||
878008e93b | |||
0cd551d4fd | |||
f85194ec46 | |||
271489bdfd | |||
bd5f22a049 | |||
189f18b112 | |||
df166184ea | |||
ce42cba096 | |||
9670863a41 | |||
1ae52bd33f | |||
c9cf9cfff0 | |||
2ffbee3db2 | |||
96b8beb9cd | |||
365b849046 | |||
8e613d03e3 | |||
b18a794eca | |||
c620c924f9 | |||
9db81a5a49 | |||
6fb7a85e8a | |||
36f81b4a62 | |||
2caecc01b2 | |||
dedb8d2d68 | |||
7192b26402 | |||
762f5bdc33 | |||
bebb52b4e8 | |||
2c9f8bb9ce | |||
efbefabb01 | |||
990fb22d3e | |||
9b2c22b2d9 | |||
df7e0d2f2f | |||
5cfda1b1bf | |||
ac9bf1f3ff | |||
7eb0868791 | |||
8a792e6d76 | |||
d8a3692d92 | |||
95ce0e39ef | |||
17b70ab38c | |||
07e76f35fa | |||
a4cab9876a | |||
c06a932c95 | |||
7d713b87b1 | |||
b1167146c5 | |||
2d0a5eb02c | |||
8d68859c2a | |||
444cefc9a2 | |||
d0deceabbd | |||
175c1df0b8 | |||
9cc6491c2a | |||
710179f4b4 | |||
d11c72fd48 | |||
0af505828e | |||
135cf9960f | |||
3bf7c74f93 | |||
cea4911c4d | |||
54dc01253d | |||
4db9a90da2 | |||
d69e9034ab | |||
71ece73d99 | |||
3bb2102eb4 | |||
b7914909d0 | |||
63398fe491 | |||
bf32bf28da | |||
dcb6bfb18d | |||
8f605dc0f6 | |||
47e770948b | |||
9ab29f5b7f | |||
10bf430ce6 | |||
67eb4e8180 | |||
141f9b7730 | |||
139a589ad6 | |||
591873a185 | |||
97a308b114 | |||
430714e67f | |||
a49adbd09c | |||
3df98d576e | |||
8135136c86 | |||
cef1c4b8a1 | |||
2e8791a101 | |||
0e2b8b10d1 | |||
3cb64669e4 | |||
bc0d32f330 | |||
0db17beacc | |||
931efed784 | |||
6378a41b6d | |||
23bf7faf9f | |||
01ff3af63f | |||
8f98055e9e | |||
84ae61f72c | |||
6dd280205b | |||
1365d553a4 | |||
61a594493c | |||
62ab70f889 | |||
eaccfdde59 | |||
a8e536478c | |||
e94d5626dd | |||
be3e31ddc4 | |||
b92b6520cb | |||
ea33179a95 | |||
6fcf6ae1f5 | |||
f2a9247b68 | |||
dc3ed7fffc | |||
271de31d51 | |||
1268caf3e0 | |||
c0cef58e39 | |||
d363d205c3 | |||
2fd5a9e883 | |||
e7ef974a39 | |||
0b62fa8b76 | |||
2d28750782 | |||
e2054a0ab7 | |||
7ae5c3b2e7 | |||
6e7fefb8b2 | |||
450bef278b | |||
0affc0d58b | |||
3d153b6c8e | |||
04fff91e23 | |||
28a23452f2 | |||
6d403851cf | |||
395a749bce | |||
2cc2a90941 | |||
c87ba6231d | |||
c5ca739b49 | |||
00fe4cdf2d | |||
69be3e1e87 | |||
2cb3984d68 | |||
5901978889 | |||
8bf1cf3cc5 | |||
f6af1184bc | |||
4880741b8b | |||
e8627800fe | |||
907fbb94a2 | |||
fd2028557e | |||
91fa1ec6b2 | |||
628c525599 | |||
bbc00768f0 | |||
5b09461ccf | |||
1a439ecece | |||
836aec4396 | |||
0b5dec9bab | |||
fd56123267 | |||
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 |
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
|
33
.github/CONTRIBUTING.md
vendored
@ -1,33 +0,0 @@
|
||||
1. **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).**
|
||||
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://github.com/inorichi/tachiyomi/wiki/Translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
|
||||
# Catalogue requests
|
||||
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Dev version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
* Include screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
|
||||
# Feature requests
|
||||
|
||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
ko_fi: inorichi
|
37
.github/ISSUE_TEMPLATE.md
vendored
@ -1,17 +1,34 @@
|
||||
**DO NOT OPEN ISSUES/REQUESTS RELATING TO EXTENSIONS/CATALOGUES IN THIS REPOSITORY. Open them at the following repository https://github.com/inorichi/tachiyomi-extensions/**
|
||||
**PLEASE READ THIS**
|
||||
|
||||
**For all other requests Please fill out the form below and remove the first 3 lines of this template**
|
||||
I acknowledge that:
|
||||
|
||||
**App version:**
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.10.12)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
**Android version:**
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**Issue/Request:**
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
**Steps to reproduce (if applicable)**
|
||||
---
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
## Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
**Other details:**
|
||||
## Steps to reproduce
|
||||
1. First step
|
||||
2. Second step
|
||||
|
||||
## Issue/Request
|
||||
?
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||
|
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
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.12)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
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/tachiyomiorg/tachiyomi-extensions
|
||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
29
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
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.12)
|
||||
- All extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## 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/tachiyomiorg/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||
labels: "catalog, invalid"
|
||||
---
|
||||
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
BIN
.github/readme-images/app-icon.png
vendored
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
.github/readme-images/screens.png
vendored
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 454 KiB |
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
|
94
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check_wrapper:
|
||||
name: Validate Gradle Wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build app
|
||||
needs: check_wrapper
|
||||
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 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
- name: Get tag name
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
id: get_tag_name
|
||||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Clean up build artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
run: |
|
||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||
body: |
|
||||
MD5: ${{ env.APK_MD5 }}
|
||||
files: |
|
||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
36
.github/workflows/issue_closer.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Issue closer
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
||||
},
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<Write short description here>*",
|
||||
"message": "The description in the title was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
}
|
||||
]
|
19
.github/workflows/lock.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: Lock threads
|
||||
|
||||
on:
|
||||
# Daily
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: '2'
|
||||
pr-lock-inactive-days: '2'
|
9
.gitignore
vendored
@ -2,8 +2,15 @@
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
.DS_Store
|
||||
/build
|
||||
.idea/
|
||||
*iml
|
||||
*.iml
|
||||
|
||||
# Built files
|
||||
*/build
|
||||
/build
|
||||
*.apk
|
||||
app/**/output.json
|
||||
|
||||
# Hebrew assets are copied on build
|
||||
app/src/main/res/values-iw/
|
||||
|
58
.travis.yml
@ -1,58 +0,0 @@
|
||||
language: android
|
||||
android:
|
||||
components:
|
||||
- build-tools-28.0.3
|
||||
- android-27
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
licenses:
|
||||
- android-sdk-license-.+
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-27" # workaround for accepting the license
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||
tar xf secrets.tar;
|
||||
mv debug.keystore "$HOME/.android";
|
||||
fi
|
||||
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
|
||||
- export ANDROID_NDK_HOME=$HOME/android-ndk-root
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
script: ".travis/build.sh"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
- "$HOME/.android/build-cache"
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||
file: tachiyomi-v*.apk
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: script
|
||||
script: ".travis/deploy.sh"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
condition: "-z $TRAVIS_TAG"
|
||||
repo: inorichi/tachiyomi
|
||||
env:
|
||||
global:
|
||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
git fetch --unshallow #required for commit count
|
||||
|
||||
cp .travis/google-services.json app/
|
||||
|
||||
if [ -z "$TRAVIS_TAG" ]; then
|
||||
./gradlew clean assembleStandardDebug
|
||||
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
|
||||
|
||||
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
|
||||
else
|
||||
./gradlew clean assembleStandardRelease
|
||||
|
||||
TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
|
||||
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
|
||||
|
||||
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
|
||||
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
|
||||
fi
|
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
pattern="tachiyomi-r*"
|
||||
files=( $pattern )
|
||||
export ARTIFACT="${files[0]}"
|
||||
|
||||
if [ -z "$ARTIFACT" ]; then
|
||||
echo "Artifact not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
|
||||
|
||||
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
|
||||
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest
|
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
34
CONTRIBUTING.md
Normal file
@ -0,0 +1,34 @@
|
||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
|
||||
|
||||
---
|
||||
|
||||
Thanks for your interest in contributing to Tachiyomi!
|
||||
|
||||
|
||||
# Code contributions
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
||||
|
||||
|
||||
# Forks
|
||||
|
||||
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
|
||||
|
||||
When creating a fork, remember to:
|
||||
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- Change the app icon
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
||||
- To avoid installation conflicts:
|
||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
||||
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
26
LICENSE
@ -174,29 +174,3 @@
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
42
README.md
@ -1,29 +1,29 @@
|
||||
| Build | Stable | Dev | Contribute | Contact |
|
||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||
|-------|----------|---------|------------|---------|
|
||||
| [](https://travis-ci.org/inorichi/tachiyomi) | [)](https://github.com/inorichi/tachiyomi/releases) | [](http://tachiyomi.kanade.eu/latest) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||
|
||||
|
||||
# Tachiyomi
|
||||
Tachiyomi is a free and open source manga reader for Android.
|
||||
Tachiyomi is a free and open source manga reader for Android 5.0 and above.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* Configurable reader with multiple viewers, reading directions and other settings
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||
* 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).
|
||||
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
||||
|
||||
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest). (auto-updates not included)
|
||||
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
|
||||
|
||||
@ -31,39 +31,49 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<details><summary>Issues</summary>
|
||||
|
||||
1. **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).**
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Bugs</summary>
|
||||
|
||||
* Include version (Setting > About > Version)
|
||||
* Include version (More > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Dev version is equal to the number of commits as seen in the main page
|
||||
* 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
|
||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Feature Requests</summary>
|
||||
|
||||
* 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)
|
||||
|
||||
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
|
||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
||||
</details>
|
||||
|
||||
<details><summary>Contributing</summary>
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
</details>
|
||||
|
||||
<details><summary>Code of Conduct</summary>
|
||||
|
||||
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||
</details>
|
||||
|
||||
## FAQ
|
||||
|
||||
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
|
||||
[See our website.](https://tachiyomi.org/)
|
||||
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||
|
||||
## License
|
||||
|
1
app/.gitignore
vendored
@ -2,4 +2,3 @@
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
google-services.json
|
||||
|
262
app/build.gradle
@ -1,262 +0,0 @@
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
|
||||
shortcutHelper.filePath = './shortcuts.xml'
|
||||
|
||||
ext {
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
getCommitCount = {
|
||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||
// return "1"
|
||||
}
|
||||
|
||||
getGitSha = {
|
||||
return 'git rev-parse --short HEAD'.execute().text.trim()
|
||||
// return "1"
|
||||
}
|
||||
|
||||
getBuildTime = {
|
||||
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||
return df.format(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '28.0.3'
|
||||
publishNonDefault true
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 27
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 41
|
||||
versionName "0.8.4"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix "-${getCommitCount()}"
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
dimension "default"
|
||||
}
|
||||
fdroid {
|
||||
dimension "default"
|
||||
}
|
||||
dev {
|
||||
minSdkVersion 21
|
||||
resConfigs "en", "xxhdpi"
|
||||
dimension "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// Android support library
|
||||
final support_library_version = '27.0.2'
|
||||
implementation "com.android.support:support-v4:$support_library_version"
|
||||
implementation "com.android.support:appcompat-v7:$support_library_version"
|
||||
implementation "com.android.support:cardview-v7:$support_library_version"
|
||||
implementation "com.android.support:design:$support_library_version"
|
||||
implementation "com.android.support:recyclerview-v7:$support_library_version"
|
||||
implementation "com.android.support:preference-v7:$support_library_version"
|
||||
implementation "com.android.support:support-annotations:$support_library_version"
|
||||
implementation "com.android.support:customtabs:$support_library_version"
|
||||
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
|
||||
|
||||
// ReactiveX
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'io.reactivex:rxjava:1.3.6'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||
|
||||
// Network client
|
||||
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
||||
implementation 'com.squareup.okio:okio:1.14.0'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.3.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// JavaScript engine
|
||||
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
||||
|
||||
// Disk
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||
|
||||
// HTML parser
|
||||
implementation 'org.jsoup:jsoup:1.10.2'
|
||||
|
||||
// Job scheduling
|
||||
implementation 'com.evernote:android-job:1.2.5'
|
||||
implementation 'com.google.android.gms:play-services-gcm:11.8.0'
|
||||
|
||||
// Changelog
|
||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
|
||||
// Database
|
||||
implementation 'android.arch.persistence:db:1.0.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.25.2'
|
||||
|
||||
// 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.6.1'
|
||||
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"
|
||||
|
||||
// Transformations
|
||||
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.6.1'
|
||||
|
||||
// Crash reports
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
// 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.0.0-rc4'
|
||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
|
||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
|
||||
implementation 'com.github.mthli:Slice:v1.2'
|
||||
implementation 'me.gujun.android.taggroup:library:1.4@aar'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.inorichi:DirectionalViewPager:3acc51a'
|
||||
|
||||
// Conductor
|
||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||
implementation ("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
|
||||
|
||||
// RxBindings
|
||||
final rxbindings_version = '1.0.1'
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
|
||||
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.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-stdlib:$kotlin_version"
|
||||
|
||||
final coroutines_version = '0.22.2'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.71'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
experimental {
|
||||
coroutines 'enable'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
333
app/build.gradle.kts
Normal file
@ -0,0 +1,333 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
android {
|
||||
compileSdkVersion(AndroidConfig.compileSdk)
|
||||
buildToolsVersion(AndroidConfig.buildTools)
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 59
|
||||
versionName = "0.10.12"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
|
||||
// Please disable ACRA or use your own instance in forked versions of the project
|
||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
ndk {
|
||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
named("release") {
|
||||
/*named("postprocessing") {
|
||||
postprocessing {
|
||||
isObfuscate = false
|
||||
isOptimizeCode = true
|
||||
isRemoveUnusedCode = false
|
||||
isRemoveUnusedResources = true
|
||||
}
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||
dimension = "default"
|
||||
}
|
||||
create("fdroid") {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resConfigs("en", "xxhdpi")
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude("META-INF/DEPENDENCIES")
|
||||
exclude("LICENSE.txt")
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
exclude("META-INF/*.kotlin_module")
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable("MissingTranslation", "ExtraTranslation")
|
||||
isAbortOnError = false
|
||||
isCheckReleaseBuilds = false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.3.2")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.3.0"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.9.1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
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.tachiyomiorg:unifile:17bec43")
|
||||
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.4")
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||
|
||||
// Dependency injection
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val glideVersion = "4.12.0"
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
|
||||
|
||||
// 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.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
val materialDialogsVersion = "3.1.1"
|
||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
||||
|
||||
// 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:2.0.1")
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "0.12.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
val robolectricVersion = "3.1.4"
|
||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.4.3"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
)
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(formatKotlin, copyHebrewStrings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
fun getCommitCount(): String {
|
||||
return runCommand("git rev-list --count HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getGitSha(): String {
|
||||
return runCommand("git rev-parse --short HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getBuildTime(): String {
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return df.format(Date())
|
||||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
77
app/proguard-rules.pro
vendored
@ -1,33 +1,26 @@
|
||||
-dontobfuscate
|
||||
|
||||
-dontwarn eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.source.model.** { *; }
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-dontwarn nucleus.view.NucleusActionBarActivity
|
||||
|
||||
# Extensions may require methods unused in the core app
|
||||
-dontwarn eu.kanade.tachiyomi.**
|
||||
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
||||
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class com.github.salomonbrys.kotson.** { *; }
|
||||
-keep class com.squareup.duktape.** { *; }
|
||||
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Design library
|
||||
-dontwarn com.google.android.material.**
|
||||
-keep class com.google.android.material.** { *; }
|
||||
-keep interface com.google.android.material.** { *; }
|
||||
-keep public class com.google.android.material.R$* { *; }
|
||||
|
||||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||
<init>();
|
||||
}
|
||||
|
||||
# RxJava 1.1.0
|
||||
@ -46,26 +39,6 @@
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
### Support v7, Design
|
||||
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
|
||||
-keep class android.support.v7.widget.RoundRectDrawable { *; }
|
||||
|
||||
-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 android.support.v7.graphics.drawable.** { *; }
|
||||
|
||||
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
-dontwarn android.support.**
|
||||
-dontwarn android.support.design.**
|
||||
-keep class android.support.design.** { *; }
|
||||
-keep interface android.support.design.** { *; }
|
||||
-keep public class android.support.design.R$* { *; }
|
||||
|
||||
|
||||
# ReactiveNetwork
|
||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
||||
|
||||
@ -84,9 +57,23 @@
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# SnakeYaml
|
||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
||||
-dontwarn org.yaml.snakeyaml.**
|
||||
|
||||
# Duktape
|
||||
-keep class com.squareup.duktape.** { *; }
|
||||
## kotlinx.serialization ##
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
||||
-keepclassmembers class eu.kanade.tachiyomi.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/sc_book_48dp"
|
||||
android:icon="@drawable/sc_collections_bookmark_48dp"
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_library"
|
||||
android:shortcutLongLabel="@string/label_library"
|
||||
@ -13,7 +13,7 @@
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/sc_update_48dp"
|
||||
android:icon="@drawable/sc_new_releases_48dp"
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_recently_updated"
|
||||
android:shortcutLongLabel="@string/label_recent_updates"
|
||||
@ -24,7 +24,7 @@
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/sc_glasses_48dp"
|
||||
android:icon="@drawable/sc_history_48dp"
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_recently_read"
|
||||
android:shortcutLongLabel="@string/label_recent_manga"
|
||||
@ -38,8 +38,8 @@
|
||||
android:icon="@drawable/sc_explore_48dp"
|
||||
android:shortcutDisabledMessage="@string/app_not_available"
|
||||
android:shortcutId="show_catalogues"
|
||||
android:shortcutLongLabel="@string/label_catalogues"
|
||||
android:shortcutShortLabel="@string/label_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" />
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
@ -2,53 +2,94 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Base"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/action_global_search">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity" />
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme" />
|
||||
android:name=".ui.security.BiometricUnlockActivity"
|
||||
android:theme="@style/Theme.Base" />
|
||||
<activity
|
||||
android:name=".ui.setting.AnilistLoginActivity"
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@ -62,8 +103,22 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.ShikomoriLoginActivity"
|
||||
android:label="Shikomori">
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:label="MyAnimeList">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="myanimelist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -75,13 +130,27 @@
|
||||
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
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
@ -108,11 +177,11 @@
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupCreateService"
|
||||
android:exported="false"/>
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false"/>
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
|
@ -3,41 +3,58 @@ package eu.kanade.tachiyomi
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.support.multidex.MultiDex
|
||||
import com.evernote.android.job.JobManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
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.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
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.ReportsCrashes
|
||||
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.registry.default.DefaultRegistrar
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.Security
|
||||
|
||||
@ReportsCrashes(
|
||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||
buildConfigClass = BuildConfig::class,
|
||||
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||
@AcraCore(
|
||||
buildConfigClass = BuildConfig::class,
|
||||
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
||||
)
|
||||
open class App : Application() {
|
||||
@AcraHttpSender(
|
||||
uri = BuildConfig.ACRA_URI,
|
||||
httpMethod = HttpSender.Method.PUT
|
||||
)
|
||||
open class App : Application(), LifecycleObserver {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
|
||||
Injekt = InjektScope(DefaultRegistrar())
|
||||
// TLS 1.3 support for Android < 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
setupNotificationChannels()
|
||||
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
// Reset Incognito Mode on relaunch
|
||||
preferences.incognitoMode().set(false)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@ -50,27 +67,21 @@ open class App : Application() {
|
||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
||||
}
|
||||
|
||||
protected open fun setupAcra() {
|
||||
ACRA.init(this)
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
if (preferences.lockAppAfter().get() >= 0) {
|
||||
SecureActivityDelegate.locked = true
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setupJobManager() {
|
||||
try {
|
||||
JobManager.create(this).addJobCreator { tag ->
|
||||
when (tag) {
|
||||
LibraryUpdateJob.TAG -> LibraryUpdateJob()
|
||||
UpdaterJob.TAG -> UpdaterJob()
|
||||
BackupCreatorJob.TAG -> BackupCreatorJob()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w("Can't initialize job manager")
|
||||
protected open fun setupAcra() {
|
||||
if (BuildConfig.FLAVOR != "dev") {
|
||||
ACRA.init(this)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setupNotificationChannels() {
|
||||
Notifications.createChannels(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
@ -11,14 +12,16 @@ 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 rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.api.*
|
||||
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) }
|
||||
@ -41,22 +44,19 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
Handler().post {
|
||||
get<PreferencesHelper>()
|
||||
|
||||
rxAsync { get<PreferencesHelper>() }
|
||||
get<NetworkHelper>()
|
||||
|
||||
rxAsync { get<NetworkHelper>() }
|
||||
get<SourceManager>()
|
||||
|
||||
rxAsync { get<SourceManager>() }
|
||||
|
||||
rxAsync { get<DatabaseHelper>() }
|
||||
|
||||
rxAsync { get<DownloadManager>() }
|
||||
get<DatabaseHelper>()
|
||||
|
||||
get<DownloadManager>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun rxAsync(block: () -> Unit) {
|
||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,21 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
@ -16,18 +28,33 @@ object Migrations {
|
||||
*/
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||
|
||||
// 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)
|
||||
|
||||
if (oldVersion == 0) return false
|
||||
// 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 && preferences.automaticUpdates()) {
|
||||
UpdaterJob.setupTask()
|
||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||
if (BuildConfig.INCLUDE_UPDATER) {
|
||||
UpdaterJob.setupTask(context)
|
||||
}
|
||||
LibraryUpdateJob.setupTask()
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
// Delete internal chapter cache dir.
|
||||
@ -39,7 +66,7 @@ object Migrations {
|
||||
if (oldDir.exists()) {
|
||||
val destDir = context.getExternalFilesDir("covers")
|
||||
if (destDir != null) {
|
||||
oldDir.listFiles().forEach {
|
||||
oldDir.listFiles()?.forEach {
|
||||
it.renameTo(File(destDir, it.name))
|
||||
}
|
||||
}
|
||||
@ -55,9 +82,76 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
@Suppress("DEPRECATION")
|
||||
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")
|
||||
}
|
||||
}
|
||||
if (oldVersion < 54) {
|
||||
// Force MAL log out due to login flow change
|
||||
// v52: switched from scraping to WebView
|
||||
// v53: switched from WebView to OAuth
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
if (trackManager.myAnimeList.isLogged) {
|
||||
trackManager.myAnimeList.logout()
|
||||
context.toast(R.string.myanimelist_relogin)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 57) {
|
||||
// Migrate DNS over HTTPS setting
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||
if (wasDohEnabled) {
|
||||
prefs.edit {
|
||||
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
||||
remove("enable_doh")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 59) {
|
||||
// Reset rotation to Free after replacing Lock
|
||||
preferences.rotation().set(1)
|
||||
|
||||
// Disable update check for Android 5.x users
|
||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
UpdaterJob.cancelTask(context)
|
||||
}
|
||||
}
|
||||
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,96 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class AbstractBackupManager(protected val context: Context) {
|
||||
|
||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||
internal val sourceManager: SourceManager by injectLazy()
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
protected val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
*
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Fetches chapter information.
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters list of chapters in the backup
|
||||
* @return Updated manga chapters.
|
||||
*/
|
||||
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source)
|
||||
if (syncedChapters.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
updateChapters(chapters)
|
||||
}
|
||||
return syncedChapters
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list containing manga from library
|
||||
*
|
||||
* @return [Manga] from library
|
||||
*/
|
||||
protected fun getFavoriteManga(): List<Manga> =
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
internal fun insertManga(manga: Manga): Long? =
|
||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
*/
|
||||
protected fun insertChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.insertChapters(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters
|
||||
*/
|
||||
protected fun updateChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters with known database ids
|
||||
*/
|
||||
protected fun updateKnownChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of backups.
|
||||
*
|
||||
* @return number of backups selected by user
|
||||
*/
|
||||
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||
}
|
@ -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 eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import kotlinx.coroutines.Job
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
|
||||
|
||||
protected val db: DatabaseHelper by injectLazy()
|
||||
protected val trackManager: TrackManager by injectLazy()
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
protected lateinit var backupManager: T
|
||||
|
||||
protected var restoreAmount = 0
|
||||
protected var restoreProgress = 0
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
protected var sourceMapping: Map<Long, String> = emptyMap()
|
||||
|
||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
abstract suspend fun performRestore(uri: Uri): Boolean
|
||||
|
||||
suspend fun restoreBackup(uri: Uri): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
|
||||
if (!performRestore(uri)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
|
||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches chapter information.
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga chapters.
|
||||
*/
|
||||
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||
return try {
|
||||
backupManager.restoreChapters(source, manga, chapters)
|
||||
} catch (e: Exception) {
|
||||
// If there's any error, return empty update and continue.
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
context.getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes tracking information.
|
||||
*
|
||||
* @param manga manga that needs updating.
|
||||
* @param tracks list containing tracks from restore file.
|
||||
*/
|
||||
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
} else {
|
||||
val serviceName = service?.nameRes()?.let { context.getString(it) }
|
||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update dialog in [BackupConst]
|
||||
*
|
||||
* @param progress restore progress
|
||||
* @param amount total restoreAmount of manga
|
||||
* @param title title of restored manga
|
||||
*/
|
||||
internal fun showRestoreProgress(
|
||||
progress: Int,
|
||||
amount: Int,
|
||||
title: String
|
||||
) {
|
||||
notifier.showRestoreProgress(title, progress, amount)
|
||||
}
|
||||
|
||||
internal fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
}
|
@ -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>)
|
||||
}
|
@ -1,23 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
object BackupConst {
|
||||
|
||||
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||
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
|
||||
}
|
@ -1,25 +1,26 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.app.IntentService
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonArray
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
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
|
||||
|
||||
/**
|
||||
* [IntentService] used to backup [Manga] information to [JsonArray]
|
||||
* Service for backing up library information to a JSON file.
|
||||
*/
|
||||
class BackupCreateService : IntentService(NAME) {
|
||||
class BackupCreateService : Service() {
|
||||
|
||||
companion object {
|
||||
// Name of class
|
||||
private const val NAME = "BackupCreateService"
|
||||
|
||||
// Options for backup
|
||||
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||
|
||||
// Filter options
|
||||
internal const val BACKUP_CATEGORY = 0x1
|
||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||
@ -31,6 +32,15 @@ class BackupCreateService : IntentService(NAME) {
|
||||
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
|
||||
*
|
||||
@ -38,26 +48,75 @@ class BackupCreateService : IntentService(NAME) {
|
||||
* @param uri path of Uri
|
||||
* @param flags determines what to backup
|
||||
*/
|
||||
fun makeBackup(context: Context, uri: Uri, flags: Int) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(EXTRA_FLAGS, flags)
|
||||
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)
|
||||
}
|
||||
context.startService(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, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
||||
stopSelf(startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private val backupManager by lazy { BackupManager(this) }
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
// Get values
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||
// Create backup
|
||||
backupManager.createBackup(uri, flags, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,42 +1,57 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.net.Uri
|
||||
import com.evernote.android.job.Job
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobRequest
|
||||
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 eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackupCreatorJob : Job() {
|
||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
Worker(context, workerParams) {
|
||||
|
||||
override fun onRunJob(params: Params): Result {
|
||||
override fun doWork(): Result {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val backupManager = BackupManager(context)
|
||||
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
|
||||
val uri = preferences.backupsDirectory().get().toUri()
|
||||
val flags = BackupCreateService.BACKUP_ALL
|
||||
backupManager.createBackup(uri, flags, true)
|
||||
return Result.SUCCESS
|
||||
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 {
|
||||
const val TAG = "BackupCreator"
|
||||
private const val TAG = "BackupCreator"
|
||||
|
||||
fun setupTask(prefInterval: Int? = null) {
|
||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||
if (interval > 0) {
|
||||
JobRequest.Builder(TAG)
|
||||
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||
.setUpdateCurrent(true)
|
||||
.build()
|
||||
.schedule()
|
||||
}
|
||||
}
|
||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||
interval.toLong(),
|
||||
TimeUnit.HOURS,
|
||||
10,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
fun cancelTask() {
|
||||
JobManager.instance().cancelAllForTag(TAG)
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||
} else {
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,152 @@
|
||||
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)
|
||||
setOnlyAlertOnce(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)
|
||||
}
|
||||
|
||||
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, isLegacyFormat: Boolean) {
|
||||
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
|
||||
clearActions()
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, 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
|
||||
clearActions()
|
||||
|
||||
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
|
||||
clearActions()
|
||||
|
||||
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||
val destFile = File(path, file)
|
||||
val uri = destFile.getUriCompat(context)
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_show_errors),
|
||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||
)
|
||||
}
|
||||
|
||||
show(Notifications.ID_RESTORE_COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
@ -6,40 +6,23 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Restores backup from json file
|
||||
* Restores backup.
|
||||
*/
|
||||
class BackupRestoreService : Service() {
|
||||
|
||||
@ -51,8 +34,8 @@ class BackupRestoreService : Service() {
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
private fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(BackupRestoreService::class.java)
|
||||
fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(BackupRestoreService::class.java)
|
||||
|
||||
/**
|
||||
* Starts a service to restore a backup from Json
|
||||
@ -60,12 +43,13 @@ class BackupRestoreService : Service() {
|
||||
* @param context context of application
|
||||
* @param uri path of Uri
|
||||
*/
|
||||
fun start(context: Context, uri: Uri) {
|
||||
fun start(context: Context, uri: Uri, mode: Int) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||
}
|
||||
context.startService(intent)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +60,8 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||
|
||||
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled))
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,66 +70,36 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscription where the update is done.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
private var backupRestore: AbstractBackupRestore<*>? = null
|
||||
private lateinit var notifier: BackupNotifier
|
||||
|
||||
/**
|
||||
* The progress of a backup restore
|
||||
*/
|
||||
private var restoreProgress = 0
|
||||
|
||||
/**
|
||||
* Amount of manga in Json file (needed for restore)
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
|
||||
/**
|
||||
* List containing errors
|
||||
*/
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
/**
|
||||
* Backup manager
|
||||
*/
|
||||
private lateinit var backupManager: BackupManager
|
||||
|
||||
/**
|
||||
* Database
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Tracking manager
|
||||
*/
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
|
||||
|
||||
private lateinit var executor: ExecutorService
|
||||
|
||||
/**
|
||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||
wakeLock.acquire()
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is destroyed. It destroys the running subscription and
|
||||
* releases the wake lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
subscription?.unsubscribe()
|
||||
executor.shutdown() // must be called after unsubscribe
|
||||
destroyJob()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun destroyJob() {
|
||||
backupRestore?.job?.cancel()
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,286 +116,34 @@ class BackupRestoreService : Service() {
|
||||
* @return the start value of the command.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return Service.START_NOT_STICKY
|
||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
// Cancel any previous job if needed.
|
||||
backupRestore?.job?.cancel()
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
backupRestore = when (mode) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
||||
else -> LegacyBackupRestore(this, notifier)
|
||||
}
|
||||
|
||||
subscription = Observable.using(
|
||||
{ db.lowLevel().beginTransaction() },
|
||||
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||
.doAfterTerminate { stopSelf(startId) }
|
||||
.subscribeOn(Schedulers.from(executor))
|
||||
.subscribe()
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
backupRestore?.writeErrorLog()
|
||||
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Observable] containing restore process.
|
||||
*
|
||||
* @param uri restore file
|
||||
* @return [Observable<Manga>]
|
||||
*/
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
return Observable.just(Unit)
|
||||
.map {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let {
|
||||
backupManager.restoreCategories(it.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
|
||||
mangasJson
|
||||
}
|
||||
.flatMap { Observable.from(it) }
|
||||
.concatMap {
|
||||
val obj = it.asJsonObject
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||
|
||||
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
||||
if (observable != null) {
|
||||
observable
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||
restoreProgress += 1
|
||||
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
||||
Observable.just(manga)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.doOnNext {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
val logFile = writeErrorLog()
|
||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_TIME, time)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(completeIntent)
|
||||
|
||||
}
|
||||
.doOnError { error ->
|
||||
Timber.e(error)
|
||||
writeErrorLog()
|
||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||
}
|
||||
sendLocalBroadcast(errorIntent)
|
||||
}
|
||||
.onErrorReturn { emptyList() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Write errors to error log
|
||||
*/
|
||||
private fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
||||
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
|
||||
notifier.showRestoreError(exception.message)
|
||||
stopSelf(startId)
|
||||
}
|
||||
val job = ioScope.launch(handler) {
|
||||
if (backupRestore?.restoreBackup(uri) == false) {
|
||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga restore observable
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
* @return [Observable] containing manga restore information
|
||||
*/
|
||||
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga>? {
|
||||
// Get source
|
||||
val source = backupManager.sourceManager.getOrStub(manga.source)
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
return if (dbManga == null) {
|
||||
// Manga not in database
|
||||
mangaFetchObservable(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
|
||||
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||
job.invokeOnCompletion {
|
||||
stopSelf(startId)
|
||||
}
|
||||
backupRestore?.job = job
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* [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 mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga> {
|
||||
return 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)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
||||
categories: List<String>, history: List<DHistory>,
|
||||
tracks: List<Track>): Observable<Manga> {
|
||||
|
||||
return 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)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(manga, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches chapter information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that refreshes tracking information
|
||||
* @param manga manga that needs updating.
|
||||
* @param tracks list containing tracks from restore file.
|
||||
* @return [Observable] that contains updated track item
|
||||
*/
|
||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||
return Observable.from(tracks)
|
||||
.concatMap { 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} - ${service?.name} not logged in")
|
||||
Observable.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update dialog in [BackupConst]
|
||||
*
|
||||
* @param progress restore progress
|
||||
* @param amount total restoreAmount of manga
|
||||
* @param title title of restored manga
|
||||
*/
|
||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,352 @@
|
||||
package eu.kanade.tachiyomi.data.backup.full
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.sink
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
|
||||
val parser = ProtoBuf
|
||||
|
||||
/**
|
||||
* Create backup Json file from database
|
||||
*
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||
// Create root object
|
||||
var backup: Backup? = null
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
val databaseManga = getFavoriteManga()
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
backupCategories(),
|
||||
backupExtensionInfo(databaseManga)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
val file: UniFile = (
|
||||
if (isJob) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
|
||||
// Delete older backups
|
||||
val numberOfBackups = numberOfBackups()
|
||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
|
||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.name }
|
||||
.drop(numberOfBackups - 1)
|
||||
.forEach { it.delete() }
|
||||
|
||||
// Create new file to place backup
|
||||
dir.createFile(BackupFull.getDefaultFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||
return file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||
return mangas.map {
|
||||
backupMangaObject(it, flags)
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
|
||||
return mangas
|
||||
.asSequence()
|
||||
.map { it.source }
|
||||
.distinct()
|
||||
.map { sourceManager.getOrStub(it) }
|
||||
.map { BackupSource.copyFrom(it) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup the categories of library
|
||||
*
|
||||
* @return list of [BackupCategory] to be backed up
|
||||
*/
|
||||
private fun backupCategories(): List<BackupCategory> {
|
||||
return databaseHelper.getCategories()
|
||||
.executeAsBlocking()
|
||||
.map { BackupCategory.copyFrom(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a manga to Json
|
||||
*
|
||||
* @param manga manga that gets converted
|
||||
* @param options options for the backup
|
||||
* @return [BackupManga] containing manga in a serializable form
|
||||
*/
|
||||
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
|
||||
// Entry for this manga
|
||||
val mangaObject = BackupManga.copyFrom(manga)
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
if (chapters.isNotEmpty()) {
|
||||
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isNotEmpty()) {
|
||||
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||
if (historyForManga.isNotEmpty()) {
|
||||
val history = historyForManga.mapNotNull { history ->
|
||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
url?.let { BackupHistory(url, history.last_read) }
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
mangaObject.history = history
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mangaObject
|
||||
}
|
||||
|
||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||
manga.id = dbManga.id
|
||||
manga.copyFrom(dbManga)
|
||||
insertManga(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
fun restoreManga(manga: Manga): Manga {
|
||||
return manga.also {
|
||||
it.initialized = it.description != null
|
||||
it.id = insertManga(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the categories from Json
|
||||
*
|
||||
* @param backupCategories list containing categories
|
||||
*/
|
||||
internal fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||
// Get categories from file and from db
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
|
||||
// Iterate over them
|
||||
backupCategories.map { it.getCategoryImpl() }.forEach { category ->
|
||||
// Used to know if the category is already in the db
|
||||
var found = false
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.name == dbCategory.name) {
|
||||
category.id = dbCategory.id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// If the category isn't in the db, remove the id and insert a new category
|
||||
// Store the inserted id in the category
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
category.id = null
|
||||
val result = databaseHelper.insertCategory(category).executeAsBlocking()
|
||||
category.id = result.insertedId()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||
categories.forEach { backupCategoryOrder ->
|
||||
backupCategories.firstOrNull {
|
||||
it.order == backupCategoryOrder
|
||||
}?.let { backupCategory ->
|
||||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history from Json
|
||||
*
|
||||
* @param history list containing history to be restored
|
||||
*/
|
||||
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
||||
// List containing history to be updated
|
||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory.apply {
|
||||
last_read = max(lastRead, dbHistory.last_read)
|
||||
}
|
||||
historyToBeUpdated.add(dbHistory)
|
||||
} else {
|
||||
// If not in database create
|
||||
databaseHelper.getChapter(url).executeAsBlocking()?.let {
|
||||
val historyToAdd = History.create(it).apply {
|
||||
last_read = lastRead
|
||||
}
|
||||
historyToBeUpdated.add(historyToAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
tracks.map { it.manga_id = manga.id!! }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = mutableListOf<Track>()
|
||||
|
||||
tracks.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.sync_id == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
if (track.media_id != dbTrack.media_id) {
|
||||
dbTrack.media_id = track.media_id
|
||||
}
|
||||
if (track.library_id != dbTrack.library_id) {
|
||||
dbTrack.library_id = track.library_id
|
||||
}
|
||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
trackToUpdate.add(dbTrack)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
track.id = null
|
||||
trackToUpdate.add(track)
|
||||
}
|
||||
}
|
||||
// Update database
|
||||
if (trackToUpdate.isNotEmpty()) {
|
||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
if (dbChapter != null) {
|
||||
chapter.id = dbChapter.id
|
||||
chapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !chapter.read) {
|
||||
chapter.read = dbChapter.read
|
||||
chapter.last_page_read = dbChapter.last_page_read
|
||||
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
||||
chapter.last_page_read = dbChapter.last_page_read
|
||||
}
|
||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
||||
chapter.bookmark = dbChapter.bookmark
|
||||
}
|
||||
}
|
||||
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
val newChapters = chapters.groupBy { it.id != null }
|
||||
newChapters[true]?.let { updateKnownChapters(it) }
|
||||
newChapters[false]?.let { insertChapters(it) }
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package eu.kanade.tachiyomi.data.backup.full
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
import java.util.Date
|
||||
|
||||
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
backupManager = FullBackupManager(context)
|
||||
|
||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
|
||||
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
||||
|
||||
// Restore categories
|
||||
if (backup.backupCategories.isNotEmpty()) {
|
||||
restoreCategories(backup.backupCategories)
|
||||
}
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||
|
||||
// Restore individual manga
|
||||
backup.backupManga.forEach {
|
||||
if (job?.isActive != true) {
|
||||
return false
|
||||
}
|
||||
|
||||
restoreManga(it, backup.backupCategories)
|
||||
}
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||
db.inTransaction {
|
||||
backupManager.restoreCategories(backupCategories)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||
}
|
||||
|
||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories
|
||||
val history = backupManga.history
|
||||
val tracks = backupManga.getTrackingImpl()
|
||||
|
||||
try {
|
||||
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga restore observable
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private fun restoreMangaData(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
) {
|
||||
db.inTransaction {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
// Manga in database
|
||||
// Copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private fun restoreMangaFetch(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.restoreManga(manga)
|
||||
fetchedManga.id ?: return
|
||||
|
||||
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreMangaNoFetch(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>
|
||||
) {
|
||||
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(manga, tracks)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.data.backup.full
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
|
||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
*
|
||||
* @throws Exception if manga cannot be found.
|
||||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
override fun validate(context: Context, uri: Uri): Results {
|
||||
val backupManager = FullBackupManager(context)
|
||||
|
||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
|
||||
if (backup.backupManga.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
|
||||
val trackers = backup.backupManga
|
||||
.flatMap { it.tracking }
|
||||
.map { it.syncId }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
.filter { !it.isLogged }
|
||||
.map { context.getString(it.nameRes()) }
|
||||
.sorted()
|
||||
|
||||
return Results(missingSources, missingTrackers)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
package eu.kanade.tachiyomi.data.backup.legacy
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.*
|
||||
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
|
||||
@ -14,81 +21,47 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
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.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.max
|
||||
|
||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
internal val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Tracking manager
|
||||
*/
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Version of parser
|
||||
*/
|
||||
var version: Int = version
|
||||
private set
|
||||
|
||||
/**
|
||||
* Json Parser
|
||||
*/
|
||||
var parser: Gson = initParser()
|
||||
|
||||
/**
|
||||
* Preferences
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Set version of parser
|
||||
*
|
||||
* @param version version of parser
|
||||
*/
|
||||
internal fun setVersion(version: Int) {
|
||||
this.version = version
|
||||
parser = initParser()
|
||||
}
|
||||
|
||||
private fun initParser(): Gson = when (version) {
|
||||
1 -> GsonBuilder().create()
|
||||
val parser: Gson = when (version) {
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
.create()
|
||||
else -> throw Exception("Json version unknown")
|
||||
.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")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +70,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @param uri path of Uri
|
||||
* @param isJob backup called from job
|
||||
*/
|
||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean) {
|
||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
@ -107,73 +80,78 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
// Create category array
|
||||
val categoryEntries = JsonArray()
|
||||
|
||||
// Create extension ID/name mapping
|
||||
val extensionEntries = JsonArray()
|
||||
|
||||
// Add value's to root
|
||||
root[Backup.VERSION] = Backup.CURRENT_VERSION
|
||||
root[Backup.VERSION] = CURRENT_VERSION
|
||||
root[Backup.MANGAS] = mangaEntries
|
||||
root[CATEGORIES] = categoryEntries
|
||||
root[EXTENSIONS] = extensionEntries
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
// Get manga from database
|
||||
val mangas = getFavoriteManga()
|
||||
|
||||
val extensions: MutableSet<String> = mutableSetOf()
|
||||
|
||||
// Backup library manga and its dependencies
|
||||
mangas.forEach { manga ->
|
||||
mangaEntries.add(backupMangaObject(manga, flags))
|
||||
|
||||
// Maintain set of extensions/sources used (excludes local source)
|
||||
if (manga.source != LocalSource.ID) {
|
||||
sourceManager.get(manga.source)?.let {
|
||||
extensions.add("${manga.source}:${it.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||
backupCategories(categoryEntries)
|
||||
}
|
||||
|
||||
// Backup extension ID/name mapping
|
||||
backupExtensionInfo(extensionEntries, extensions)
|
||||
}
|
||||
|
||||
try {
|
||||
// When BackupCreatorJob
|
||||
if (isJob) {
|
||||
// Get dir of file and create
|
||||
var dir = UniFile.fromUri(context, uri)
|
||||
dir = dir.createDirectory("automatic")
|
||||
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) }
|
||||
// Delete older backups
|
||||
val numberOfBackups = numberOfBackups()
|
||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||
.orEmpty()
|
||||
.sortedByDescending { it.name }
|
||||
.drop(numberOfBackups - 1)
|
||||
.forEach { it.delete() }
|
||||
|
||||
// Create new file to place backup
|
||||
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
// Create new file to place backup
|
||||
dir.createFile(Backup.getDefaultFilename())
|
||||
} else {
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
newFile.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
} else {
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
|
||||
// Show completed dialog
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
|
||||
}
|
||||
context.sendLocalBroadcast(intent)
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
return file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
if (!isJob) {
|
||||
// Show error dialog
|
||||
val intent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
|
||||
}
|
||||
context.sendLocalBroadcast(intent)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||
extensions.sorted().forEach {
|
||||
root.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +182,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||
// Backup all the chapters
|
||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
if (!chapters.isEmpty()) {
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chaptersJson = parser.toJsonTree(chapters)
|
||||
if (chaptersJson.asJsonArray.size() > 0) {
|
||||
entry[CHAPTERS] = chaptersJson
|
||||
@ -216,7 +194,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (!categoriesForManga.isEmpty()) {
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
val categoriesNames = categoriesForManga.map { it.name }
|
||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||
}
|
||||
@ -225,7 +203,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
// Check if user wants track information in backup
|
||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
if (!tracks.isEmpty()) {
|
||||
if (tracks.isNotEmpty()) {
|
||||
entry[TRACK] = parser.toJsonTree(tracks)
|
||||
}
|
||||
}
|
||||
@ -233,7 +211,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
// 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.isEmpty()) {
|
||||
if (historyForManga.isNotEmpty()) {
|
||||
val historyData = historyForManga.mapNotNull { history ->
|
||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||
url?.let { DHistory(url, history.last_read) }
|
||||
@ -256,39 +234,20 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
* @return Updated manga.
|
||||
*/
|
||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches chapter information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||
.doOnNext {
|
||||
if (it.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
suspend fun fetchManga(source: Source, manga: Manga): Manga {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
return manga.also {
|
||||
it.copyFrom(networkManga.toSManga())
|
||||
it.favorite = true
|
||||
it.initialized = true
|
||||
it.id = insertManga(manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -308,7 +267,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.nameLower == dbCategory.nameLower) {
|
||||
if (category.name == dbCategory.name) {
|
||||
category.id = dbCategory.id
|
||||
found = true
|
||||
break
|
||||
@ -333,10 +292,10 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
||||
if (backupCategoryStr == dbCategory.name) {
|
||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||
break
|
||||
}
|
||||
@ -344,10 +303,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||
val mangaAsList = ArrayList<Manga>()
|
||||
mangaAsList.add(manga)
|
||||
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
|
||||
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
@ -359,13 +316,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||
// List containing history to be updated
|
||||
val historyToBeUpdated = ArrayList<History>()
|
||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory.apply {
|
||||
last_read = Math.max(lastRead, dbHistory.last_read)
|
||||
last_read = max(lastRead, dbHistory.last_read)
|
||||
}
|
||||
historyToBeUpdated.add(dbHistory)
|
||||
} else {
|
||||
@ -388,14 +345,14 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
tracks.map { it.manga_id = manga.id!! }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = ArrayList<Track>()
|
||||
val trackToUpdate = ArrayList<Track>(tracks.size)
|
||||
|
||||
tracks.forEach { track ->
|
||||
// Fix foreign keys with the current manga id
|
||||
track.manga_id = manga.id!!
|
||||
|
||||
for (track in tracks) {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
var isInDatabase = false
|
||||
@ -408,7 +365,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
if (track.library_id != dbTrack.library_id) {
|
||||
dbTrack.library_id = track.library_id
|
||||
}
|
||||
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||
isInDatabase = true
|
||||
trackToUpdate.add(dbTrack)
|
||||
break
|
||||
@ -422,7 +379,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
}
|
||||
// Update database
|
||||
if (!trackToUpdate.isEmpty()) {
|
||||
if (trackToUpdate.isNotEmpty()) {
|
||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
@ -438,8 +395,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
// Return if fetch is needed
|
||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
|
||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (chapter in chapters) {
|
||||
val pos = dbChapters.indexOf(chapter)
|
||||
@ -449,50 +407,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
chapter.copyFrom(dbChapter)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Filter the chapters that couldn't be found.
|
||||
chapters.filter { it.id != null }
|
||||
chapters.map { it.manga_id = manga.id }
|
||||
|
||||
insertChapters(chapters)
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
// Filter the chapters that couldn't be found.
|
||||
updateChapters(chapters.filter { it.id != null })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
*
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Returns list containing manga from library
|
||||
*
|
||||
* @return [Manga] from library
|
||||
*/
|
||||
internal fun getFavoriteManga(): List<Manga> =
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
internal fun insertManga(manga: Manga): Long? =
|
||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
*/
|
||||
private fun insertChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of backups.
|
||||
*
|
||||
* @return number of backups selected by user
|
||||
*/
|
||||
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
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 java.util.Date
|
||||
|
||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||
|
||||
override suspend 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 suspend 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()
|
||||
)
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
try {
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga restore observable
|
||||
*
|
||||
* @param manga manga data from json
|
||||
* @param source source to get manga data from
|
||||
* @param chapters chapters data from json
|
||||
* @param categories categories data from json
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private suspend fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
|
||||
db.inTransaction {
|
||||
if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
|
||||
} else { // Manga in database
|
||||
// Copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information.
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private suspend fun restoreMangaFetch(
|
||||
source: Source,
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
try {
|
||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||
fetchedManga.id ?: return
|
||||
|
||||
updateChapters(source, fetchedManga, chapters)
|
||||
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
||||
|
||||
updateTracking(fetchedManga, tracks)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreMangaNoFetch(
|
||||
source: Source,
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
}
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks)
|
||||
|
||||
updateTracking(backupManga, tracks)
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(manga, tracks)
|
||||
}
|
||||
}
|
@ -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 { context.getString(it.nameRes()) }
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.backup.models
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Json values
|
||||
@ -13,6 +14,7 @@ object Backup {
|
||||
const val TRACK = "track"
|
||||
const val CHAPTERS = "chapters"
|
||||
const val CATEGORIES = "categories"
|
||||
const val EXTENSIONS = "extensions"
|
||||
const val HISTORY = "history"
|
||||
const val VERSION = "version"
|
||||
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||
|
||||
data class DHistory(val url: String, val lastRead: Long)
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
@ -43,9 +43,7 @@ object ChapterTypeAdapter {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
val name = nextName()
|
||||
|
||||
when (name) {
|
||||
when (nextName()) {
|
||||
URL -> chapter.url = nextString()
|
||||
READ -> chapter.read = nextInt() == 1
|
||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
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.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||
|
||||
/**
|
||||
* JSON Serializer used to write / read [DHistory] to / from json
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||
|
||||
import android.telecom.DisconnectCause.REMOTE
|
||||
import com.github.salomonbrys.kotson.typeAdapter
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.stream.JsonToken
|
||||
@ -42,9 +41,7 @@ object TrackTypeAdapter {
|
||||
beginObject()
|
||||
while (hasNext()) {
|
||||
if (peek() == JsonToken.NAME) {
|
||||
val name = nextName()
|
||||
|
||||
when (name) {
|
||||
when (nextName()) {
|
||||
TITLE -> track.title = nextString()
|
||||
SYNC -> track.sync_id = nextInt()
|
||||
MEDIA -> track.media_id = nextInt()
|
@ -1,3 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
data class DHistory(val url: String,val lastRead: Long)
|
@ -2,16 +2,17 @@ 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.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.saveTo
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import okio.Okio
|
||||
import rx.Observable
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@ -38,22 +39,23 @@ class ChapterCache(private val context: Context) {
|
||||
const val PARAMETER_VALUE_COUNT = 1
|
||||
|
||||
/** The maximum number of bytes this cache should use to store. */
|
||||
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
|
||||
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
|
||||
}
|
||||
|
||||
/** Google Json class used for parsing JSON files. */
|
||||
private val gson: Gson by injectLazy()
|
||||
private val json: Json 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)
|
||||
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
|
||||
private val cacheDir: File
|
||||
get() = diskCache.directory
|
||||
|
||||
/**
|
||||
@ -68,42 +70,19 @@ class ChapterCache(private val context: Context) {
|
||||
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
|
||||
|
||||
try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
return diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page list from cache.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
* @return an observable of the list of pages.
|
||||
* @return 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))
|
||||
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
||||
// 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))
|
||||
}
|
||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||
return diskCache.get(key).use {
|
||||
json.decodeFromString(it.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +94,7 @@ class ChapterCache(private val context: Context) {
|
||||
*/
|
||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||
// Convert list of pages to json string.
|
||||
val cachedValue = gson.toJson(pages)
|
||||
val cachedValue = json.encodeToString(pages)
|
||||
|
||||
// Initialize the editor (edits the values for an entry).
|
||||
var editor: DiskLruCache.Editor? = null
|
||||
@ -126,7 +105,7 @@ class ChapterCache(private val context: Context) {
|
||||
editor = diskCache.edit(key) ?: return
|
||||
|
||||
// Write chapter urls to cache.
|
||||
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
|
||||
editor.newOutputStream(0).sink().buffer().use {
|
||||
it.write(cachedValue.toByteArray())
|
||||
it.flush()
|
||||
}
|
||||
@ -134,7 +113,6 @@ class ChapterCache(private val context: Context) {
|
||||
diskCache.flush()
|
||||
editor.commit()
|
||||
editor.abortUnlessCommitted()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Ignore.
|
||||
} finally {
|
||||
@ -149,10 +127,10 @@ class ChapterCache(private val context: Context) {
|
||||
* @return true if in cache otherwise false.
|
||||
*/
|
||||
fun isImageInCache(imageUrl: String): Boolean {
|
||||
try {
|
||||
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||
return try {
|
||||
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,18 +164,49 @@ class ChapterCache(private val context: Context) {
|
||||
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
|
||||
|
||||
// Get OutputStream and write image with Okio.
|
||||
response.body()!!.source().saveTo(editor.newOutputStream(0))
|
||||
response.body!!.source().saveTo(editor.newOutputStream(0))
|
||||
|
||||
diskCache.flush()
|
||||
editor.commit()
|
||||
} finally {
|
||||
response.body()?.close()
|
||||
response.body?.close()
|
||||
editor?.abortUnlessCommitted()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear(): Int {
|
||||
var deletedFiles = 0
|
||||
cacheDir.listFiles()?.forEach {
|
||||
if (removeFileFromCache(it.name)) {
|
||||
deletedFiles++
|
||||
}
|
||||
}
|
||||
return deletedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove file from cache.
|
||||
*
|
||||
* @param file name of file "md5.0".
|
||||
* @return status of deletion for the file.
|
||||
*/
|
||||
private fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
// Remove the extension from the file to get the key of the cache
|
||||
val key = file.substringBeforeLast(".")
|
||||
// Remove file from cache.
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKey(chapter: Chapter): String {
|
||||
return "${chapter.manga_id}${chapter.url}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.cache
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
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
|
||||
@ -17,51 +18,89 @@ import java.io.InputStream
|
||||
*/
|
||||
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 = context.getExternalFilesDir("covers") ?:
|
||||
File(context.filesDir, "covers").also { it.mkdirs() }
|
||||
private val cacheDir = getCacheDir(COVERS_DIR)
|
||||
|
||||
private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR)
|
||||
|
||||
/**
|
||||
* Returns the cover from cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @param manga the manga.
|
||||
* @return cover image.
|
||||
*/
|
||||
fun getCoverFile(thumbnailUrl: String): File {
|
||||
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
|
||||
fun getCoverFile(manga: Manga): File? {
|
||||
return manga.thumbnail_url?.let {
|
||||
File(cacheDir, DiskUtil.hashKeyForDisk(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the given stream to this cache.
|
||||
* Returns the custom cover from cache.
|
||||
*
|
||||
* @param thumbnailUrl url of the thumbnail.
|
||||
* @param inputStream the stream to copy.
|
||||
* @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 copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||
// Get destination file.
|
||||
val destFile = getCoverFile(thumbnailUrl)
|
||||
|
||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
|
||||
getCustomCoverFile(manga).outputStream().use {
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the cover file from the cache.
|
||||
* Delete the cover files of the manga from the cache.
|
||||
*
|
||||
* @param thumbnailUrl the thumbnail url.
|
||||
* @return status of deletion.
|
||||
* @param manga the manga.
|
||||
* @param deleteCustomCover whether the custom cover should be deleted.
|
||||
* @return number of files that were deleted.
|
||||
*/
|
||||
fun deleteFromCache(thumbnailUrl: String?): Boolean {
|
||||
// Check if url is empty.
|
||||
if (thumbnailUrl.isNullOrEmpty())
|
||||
return false
|
||||
fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int {
|
||||
var deleted = 0
|
||||
|
||||
// Remove file.
|
||||
val file = getCoverFile(thumbnailUrl!!)
|
||||
return file.exists() && file.delete()
|
||||
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,18 +1,33 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.database.queries.*
|
||||
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 {
|
||||
open class DatabaseHelper(context: Context) :
|
||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||
|
||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
@ -20,17 +35,14 @@ open class DatabaseHelper(context: Context)
|
||||
.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()
|
||||
.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)
|
||||
|
||||
fun lowLevel() = db.lowLevel()
|
||||
|
||||
}
|
||||
|
@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||
lowLevel().endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import android.arch.persistence.db.SupportSQLiteDatabase
|
||||
import android.arch.persistence.db.SupportSQLiteOpenHelper
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import eu.kanade.tachiyomi.data.database.tables.*
|
||||
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) {
|
||||
|
||||
@ -18,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
/**
|
||||
* Version of the database.
|
||||
*/
|
||||
const val DATABASE_VERSION = 8
|
||||
const val DATABASE_VERSION = 11
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||
@ -42,8 +44,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
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""")
|
||||
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
|
||||
@ -67,10 +71,20 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
interface DbProvider {
|
||||
|
||||
val db: DefaultStorIOSQLite
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -18,29 +18,30 @@ 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()
|
||||
CategoryPutResolver(),
|
||||
CategoryGetResolver(),
|
||||
CategoryDeleteResolver()
|
||||
)
|
||||
|
||||
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.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)
|
||||
}
|
||||
override fun mapToContentValues(obj: Category) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_NAME to obj.name,
|
||||
COL_ORDER to obj.order,
|
||||
COL_FLAGS to obj.flags
|
||||
)
|
||||
}
|
||||
|
||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||
@ -56,8 +57,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -26,37 +26,38 @@ 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()
|
||||
ChapterPutResolver(),
|
||||
ChapterGetResolver(),
|
||||
ChapterDeleteResolver()
|
||||
)
|
||||
|
||||
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.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)
|
||||
}
|
||||
override fun mapToContentValues(obj: Chapter) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_URL to obj.url,
|
||||
COL_NAME to obj.name,
|
||||
COL_READ to obj.read,
|
||||
COL_SCANLATOR to obj.scanlator,
|
||||
COL_BOOKMARK to obj.bookmark,
|
||||
COL_DATE_FETCH to obj.date_fetch,
|
||||
COL_DATE_UPLOAD to obj.date_upload,
|
||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||
COL_SOURCE_ORDER to obj.source_order
|
||||
)
|
||||
}
|
||||
|
||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
@ -80,9 +81,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -18,29 +18,30 @@ 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()
|
||||
HistoryPutResolver(),
|
||||
HistoryGetResolver(),
|
||||
HistoryDeleteResolver()
|
||||
)
|
||||
|
||||
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.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)
|
||||
}
|
||||
override fun mapToContentValues(obj: History) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_CHAPTER_ID to obj.chapter_id,
|
||||
COL_LAST_READ to obj.last_read,
|
||||
COL_TIME_READ to obj.time_read
|
||||
)
|
||||
}
|
||||
|
||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||
@ -56,8 +57,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -16,28 +16,29 @@ 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()
|
||||
MangaCategoryPutResolver(),
|
||||
MangaCategoryGetResolver(),
|
||||
MangaCategoryDeleteResolver()
|
||||
)
|
||||
|
||||
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.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)
|
||||
}
|
||||
override fun mapToContentValues(obj: MangaCategory) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_CATEGORY_ID to obj.category_id
|
||||
)
|
||||
}
|
||||
|
||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||
@ -52,8 +53,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -14,6 +14,8 @@ 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
|
||||
@ -29,40 +31,43 @@ 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()
|
||||
MangaPutResolver(),
|
||||
MangaGetResolver(),
|
||||
MangaDeleteResolver()
|
||||
)
|
||||
|
||||
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(15).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)
|
||||
}
|
||||
override fun mapToContentValues(obj: Manga) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_SOURCE to obj.source,
|
||||
COL_URL to obj.url,
|
||||
COL_ARTIST to obj.artist,
|
||||
COL_AUTHOR to obj.author,
|
||||
COL_DESCRIPTION to obj.description,
|
||||
COL_GENRE to obj.genre,
|
||||
COL_TITLE to obj.title,
|
||||
COL_STATUS to obj.status,
|
||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||
COL_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
COL_INITIALIZED to obj.initialized,
|
||||
COL_VIEWER to obj.viewer,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
)
|
||||
}
|
||||
|
||||
interface BaseMangaGetResolver {
|
||||
@ -82,6 +87,8 @@ interface BaseMangaGetResolver {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,8 +102,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver
|
||||
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||
|
||||
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@ -11,12 +11,14 @@ 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
|
||||
@ -25,37 +27,39 @@ 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()
|
||||
TrackPutResolver(),
|
||||
TrackGetResolver(),
|
||||
TrackDeleteResolver()
|
||||
)
|
||||
|
||||
class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
|
||||
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
.table(TABLE)
|
||||
.build()
|
||||
|
||||
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
.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)
|
||||
|
||||
}
|
||||
override fun mapToContentValues(obj: Track) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_SYNC_ID to obj.sync_id,
|
||||
COL_MEDIA_ID to obj.media_id,
|
||||
COL_LIBRARY_ID to obj.library_id,
|
||||
COL_TITLE to obj.title,
|
||||
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
|
||||
COL_TOTAL_CHAPTERS to obj.total_chapters,
|
||||
COL_STATUS to obj.status,
|
||||
COL_TRACKING_URL to obj.tracking_url,
|
||||
COL_SCORE to obj.score,
|
||||
COL_START_DATE to obj.started_reading_date,
|
||||
COL_FINISH_DATE to obj.finished_reading_date
|
||||
)
|
||||
}
|
||||
|
||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
@ -72,14 +76,16 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
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()
|
||||
.table(TABLE)
|
||||
.where("$COL_ID = ?")
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
}
|
||||
|
@ -12,9 +12,6 @@ interface Category : Serializable {
|
||||
|
||||
var flags: Int
|
||||
|
||||
val nameLower: String
|
||||
get() = name.toLowerCase()
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(name: String): Category = CategoryImpl().apply {
|
||||
@ -23,5 +20,4 @@ interface Category : Serializable {
|
||||
|
||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||
}
|
||||
|
||||
}
|
@ -15,12 +15,10 @@ class CategoryImpl : Category {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val category = other as Category
|
||||
|
||||
return name == category.name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,11 +31,11 @@ class ChapterImpl : Chapter {
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val chapter = other as Chapter
|
||||
return url == chapter.url
|
||||
if (url != chapter.url) return false
|
||||
return id == chapter.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
return url.hashCode() + id.hashCode()
|
||||
}
|
||||
|
||||
}
|
@ -35,7 +35,7 @@ interface History : Serializable {
|
||||
* @param chapter chapter object
|
||||
* @return history object
|
||||
*/
|
||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
||||
this.chapter_id = chapter.id!!
|
||||
}
|
||||
}
|
||||
|
@ -5,5 +5,4 @@ class LibraryManga : MangaImpl() {
|
||||
var unread: Int = 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
|
||||
interface Manga : SManga {
|
||||
|
||||
@ -12,22 +13,30 @@ interface Manga : SManga {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun setFlags(flag: Int, mask: Int) {
|
||||
chapter_flags = chapter_flags and mask.inv() or (flag and 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
|
||||
@ -72,7 +81,8 @@ interface Manga : SManga {
|
||||
|
||||
const val SORTING_SOURCE = 0x00000000
|
||||
const val SORTING_NUMBER = 0x00000100
|
||||
const val SORTING_MASK = 0x00000100
|
||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
||||
const val SORTING_MASK = 0x00000300
|
||||
|
||||
const val DISPLAY_NAME = 0x00000000
|
||||
const val DISPLAY_NUMBER = 0x00100000
|
||||
@ -88,5 +98,17 @@ interface Manga : SManga {
|
||||
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
|
||||
)
|
||||
}
|
@ -17,5 +17,4 @@ class MangaCategory {
|
||||
return mc
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,6 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
*
|
||||
* @param manga object containing manga
|
||||
* @param chapter object containing chater
|
||||
* @param history object containing history
|
||||
* @param history object containing history
|
||||
*/
|
||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
||||
|
@ -26,24 +26,26 @@ open class MangaImpl : Manga {
|
||||
|
||||
override var last_update: Long = 0
|
||||
|
||||
override var date_added: Long = 0
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var viewer: Int = 0
|
||||
|
||||
override var chapter_flags: Int = 0
|
||||
|
||||
override var cover_last_modified: Long = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
val manga = other as Manga
|
||||
|
||||
return url == manga.url
|
||||
|
||||
if (url != manga.url) return false
|
||||
return id == manga.id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
return url.hashCode() + id.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,12 +24,18 @@ interface Track : Serializable {
|
||||
|
||||
var status: Int
|
||||
|
||||
var started_reading_date: Long
|
||||
|
||||
var finished_reading_date: Long
|
||||
|
||||
var tracking_url: String
|
||||
|
||||
fun copyPersonalFrom(other: Track) {
|
||||
last_chapter_read = other.last_chapter_read
|
||||
score = other.score
|
||||
status = other.status
|
||||
started_reading_date = other.started_reading_date
|
||||
finished_reading_date = other.finished_reading_date
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -37,5 +43,4 @@ interface Track : Serializable {
|
||||
sync_id = serviceId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ class TrackImpl : Track {
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var started_reading_date: Long = 0
|
||||
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override var tracking_url: String = ""
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@ -41,5 +45,4 @@ class TrackImpl : Track {
|
||||
result = 31 * result + media_id
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
interface CategoryQueries : DbProvider {
|
||||
|
||||
fun getCategories() = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COL_ORDER)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(CategoryTable.TABLE)
|
||||
.orderBy(CategoryTable.COL_ORDER)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery())
|
||||
.args(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery())
|
||||
.args(manga.id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
@ -32,5 +36,4 @@ interface CategoryQueries : DbProvider {
|
||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||
|
||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||
|
||||
}
|
@ -7,51 +7,70 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
interface ChapterQueries : DbProvider {
|
||||
|
||||
fun getChapters(manga: Manga) = db.get()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getRecentChapters(date: Date) = db.get()
|
||||
.listOfObjects(MangaChapter::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getRecentsQuery())
|
||||
.args(date.time)
|
||||
.observesTables(ChapterTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
.listOfObjects(MangaChapter::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getRecentsQuery())
|
||||
.args(date.time)
|
||||
.observesTables(ChapterTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getChapter(id: Long) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChapter(url: String) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(url)
|
||||
.build())
|
||||
.prepare()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(url)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChapter(url: String, mangaId: Long) = db.get()
|
||||
.`object`(Chapter::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(url, mangaId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||
|
||||
@ -62,23 +81,27 @@ interface ChapterQueries : DbProvider {
|
||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||
|
||||
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterBackupPutResolver())
|
||||
.prepare()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterBackupPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterKnownBackupPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||
.`object`(chapter)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
.`object`(chapter)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
||||
.prepare()
|
||||
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
||||
.prepare()
|
||||
}
|