mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-03 05:11:31 +02:00
Compare commits
1003 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
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 | ||
|
f14af7cf83 | ||
|
4014c48c62 | ||
|
19993199db | ||
|
5b9f362925 |
.github
.gitignore.travis.yml.travis
PREVIEW_RELEASE_NOTES.mdREADME.mdapp
.gitignorebuild.gradleproguard-rules.proshortcuts.xml
build.gradlebuild.gradle.ktssrc
main
AndroidManifest.xml
assets
fonts
java
eu
kanade
tachiyomi
App.ktAppModule.ktMigrations.kt
data
backup
BackupConst.ktBackupCreateService.ktBackupCreatorJob.ktBackupManager.ktBackupNotifier.ktBackupRestoreService.ktBackupRestoreValidator.kt
models
serializer
cache
database
DatabaseHelper.ktDbExtensions.ktDbOpenCallback.ktDbProvider.kt
mappers
CategoryTypeMapping.ktChapterTypeMapping.ktHistoryTypeMapping.ktMangaCategoryTypeMapping.ktMangaTypeMapping.ktTrackTypeMapping.kt
models
Category.ktCategoryImpl.ktChapterImpl.ktHistory.ktLibraryManga.ktManga.ktMangaCategory.ktMangaChapterHistory.ktMangaImpl.ktTrack.ktTrackImpl.kt
queries
CategoryQueries.ktChapterQueries.ktHistoryQueries.ktMangaCategoryQueries.ktMangaQueries.ktRawQueries.ktTrackQueries.kt
resolvers
ChapterBackupPutResolver.ktChapterProgressPutResolver.ktChapterSourceOrderPutResolver.ktHistoryLastReadPutResolver.ktLibraryMangaGetResolver.ktMangaChapterGetResolver.ktMangaCoverLastModifiedPutResolver.ktMangaFavoritePutResolver.ktMangaFlagsPutResolver.ktMangaLastUpdatedPutResolver.ktMangaTitlePutResolver.ktMangaViewerPutResolver.kt
tables
download
DownloadCache.ktDownloadManager.ktDownloadNotifier.ktDownloadPendingDeleter.ktDownloadProvider.ktDownloadService.ktDownloadStore.ktDownloader.kt
model
glide
FileFetcher.ktLibraryMangaCustomCoverFetcher.ktLibraryMangaUrlFetcher.ktMangaSignature.ktMangaThumbnail.ktMangaThumbnailModelLoader.ktPassthroughModelLoader.ktTachiGlideModule.kt
library
notification
preference
EmptyPreferenceDataStore.ktPreferenceKeys.ktPreferenceValues.ktPreferencesHelper.ktSharedPreferencesDataStore.kt
track
TrackManager.ktTrackService.kt
anilist
bangumi
Avatar.ktBangumi.ktBangumiApi.ktBangumiInterceptor.ktBangumiModels.ktCollection.ktOAuth.ktStatus.ktUser.kt
kitsu
model
myanimelist
shikimori
updater
extension
network
AndroidCookieJar.ktCloudflareInterceptor.ktNetworkHelper.ktOkHttpExtensions.ktProgressListener.ktProgressResponseBody.ktRequests.ktUserAgentInterceptor.kt
source
ui
base
activity
controller
BaseController.ktConductorExtensions.ktDialogController.javaDialogController.ktFabController.ktNoToolbarElevationController.ktNucleusController.ktRootController.ktRxController.ktSecondaryDrawerController.ktTabbedController.kt
holder
presenter
browse
BrowseController.kt
extension
ExtensionAdapter.ktExtensionController.ktExtensionDividerItemDecoration.ktExtensionFilterController.ktExtensionGroupHolder.ktExtensionGroupItem.ktExtensionHolder.ktExtensionItem.ktExtensionPresenter.ktExtensionTrustDialog.ktExtensionViewUtils.kt
details
migration
source
LangHolder.ktLangItem.ktSourceAdapter.ktSourceController.ktSourceDividerItemDecoration.ktSourceFilterController.ktSourceHolder.ktSourceItem.ktSourcePresenter.kt
browse
BrowseSourceController.ktBrowseSourcePresenter.ktNoResultsException.ktPager.ktProgressItem.ktSourceComfortableGridHolder.ktSourceFilterSheet.ktSourceGridHolder.ktSourceHolder.ktSourceItem.ktSourceListHolder.ktSourcePager.kt
filter
CheckboxItem.ktGroupItem.ktHeaderItem.ktSectionItems.ktSelectItem.ktSeparatorItem.ktSortGroup.ktSortItem.ktTextItem.ktTriStateItem.kt
globalsearch
GlobalSearchAdapter.ktGlobalSearchCardAdapter.ktGlobalSearchCardHolder.ktGlobalSearchCardItem.ktGlobalSearchController.ktGlobalSearchHolder.ktGlobalSearchItem.ktGlobalSearchPresenter.kt
latest
catalogue
CatalogueAdapter.ktCatalogueController.ktCataloguePresenter.ktLangHolder.ktSourceHolder.ktSourceItem.kt
browse
BrowseCatalogueController.ktCatalogueItem.ktCatalogueListHolder.ktCatalogueNavigationView.ktNoResultsException.kt
global_search
latest
category
CategoryAdapter.ktCategoryController.ktCategoryCreateDialog.ktCategoryHolder.ktCategoryItem.ktCategoryPresenter.ktCategoryRenameDialog.kt
download
extension
library
ChangeMangaCategoriesDialog.ktChangeMangaCoverDialog.ktDeleteLibraryMangasDialog.ktLibraryAdapter.ktLibraryCategoryAdapter.ktLibraryCategoryView.ktLibraryComfortableGridHolder.ktLibraryCompactGridHolder.ktLibraryController.ktLibraryGridHolder.ktLibraryHolder.ktLibraryItem.ktLibraryListHolder.ktLibraryNavigationView.ktLibraryPresenter.ktLibrarySelectionEvent.ktLibrarySettingsSheet.ktLibrarySort.kt
main
manga
MangaController.ktMangaPresenter.kt
chapter
ChapterDividerItemDecoration.ktChapterHolder.ktChapterItem.ktChaptersAdapter.ktChaptersController.ktChaptersSettingsSheet.ktDeleteChaptersDialog.ktDeletingChaptersDialog.ktDownloadChaptersDialog.ktDownloadCustomChaptersDialog.ktMangaChaptersHeaderAdapter.ktSetDisplayModeDialog.ktSetSortingDialog.kt
info
MangaCoverImageView.ktMangaInfoController.ktMangaInfoHeaderAdapter.ktMangaInfoPresenter.ktMangaWebViewController.kt
track
migration
MangaAdapter.ktMangaHolder.ktMigrationController.ktMigrationPresenter.ktSearchController.ktSearchPresenter.ktSourceAdapter.ktSourceHolder.ktViewState.kt
more
reader
ChapterLoadStrategy.ktPageIndicatorTextView.ktReaderActivity.ktReaderColorFilterSheet.ktReaderColorFilterView.ktReaderPageSheet.ktReaderPresenter.ktReaderSeekBar.ktReaderSettingsSheet.ktSaveImageNotifier.kt
loader
ChapterLoader.ktDirectoryPageLoader.ktDownloadPageLoader.ktEpubPageLoader.ktHttpPageLoader.ktPageLoader.ktRarPageLoader.ktZipPageLoader.kt
model
viewer
recent
DateSectionItem.kt
history
HistoryAdapter.ktHistoryController.ktHistoryHolder.ktHistoryItem.ktHistoryPresenter.ktRemoveHistoryDialog.kt
updates
recent_updates
ConfirmDeleteChaptersDialog.ktDeletingChaptersDialog.ktRecentChapterHolder.ktRecentChaptersController.kt
recently_read
security
setting
SettingsAboutController.ktSettingsAdvancedController.ktSettingsBackupController.ktSettingsBrowseController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsMainController.ktSettingsReaderController.ktSettingsSecurityController.ktSettingsSourcesController.ktSettingsTrackingController.kt
track
webview
util
ComparatorUtil.ktCoroutinesExtensions.ktEpubFile.ktGLUtil.javaJsoupExtensions.ktMangaExtensions.ktSharedData.ktStringExtensions.ktViewExtensions.ktWebViewClientCompat.kt
chapter
lang
preference
storage
system
view
widget
ActionToolbar.ktAutofitRecyclerView.ktCustomLayoutPicker.ktDialogCheckboxView.ktDialogCustomDownloadView.ktDrawerSwipeCloseListener.ktElevationAppBarLayout.ktEmptyView.ktExtendedNavigationView.ktIgnoreFirstSpinnerListener.ktMaterialFastScroll.ktMaxHeightViewPager.ktMinMaxNumberPicker.ktNegativeSeekBar.ktOutlineSpan.ktPTSansTextView.ktPreCachingLayoutManager.ktRecyclerViewPagerAdapter.ktRevealAnimationView.ktSimpleAnimationListener.ktSimpleNavigationView.ktSimpleSeekBarListener.ktSimpleTextWatcher.ktStateImageViewTarget.ktTabbedBottomSheetDialog.ktThemedSwipeRefreshLayout.ktViewPagerAdapter.kt
preference
res
anim
color
drawable-hdpi
ic_av_pause_grey_24dp_img.pngic_av_play_arrow_grey_img.pngic_book_white_24dp.pngic_clear_grey_24dp_img.pngic_delete_grey_24dp.pngic_insert_photo_white_24dp.pngic_refresh_grey_24dp_img.pngic_refresh_white_24dp_img.pngic_share_grey_24dp.pngic_system_update_grey_24dp_img.pngsplash_icon.webptachiyomi_circle.png
drawable-mdpi
ic_av_pause_grey_24dp_img.pngic_av_play_arrow_grey_img.pngic_book_white_24dp.pngic_clear_grey_24dp_img.pngic_delete_grey_24dp.pngic_insert_photo_white_24dp.pngic_refresh_grey_24dp_img.pngic_refresh_white_24dp_img.pngic_share_grey_24dp.pngic_system_update_grey_24dp_img.pngsplash_icon.webptachiyomi_circle.png
drawable-nodpi
ic_tracker_anilist.webpic_tracker_bangumi.webpic_tracker_kitsu.webpic_tracker_mal.webpic_tracker_shikimori.webp
drawable-v21
library_item_selector_amoled.xmllibrary_item_selector_dark.xmllibrary_item_selector_light.xmllist_item_selector_amoled.xml
drawable-xhdpi
card_background.9.pngic_av_pause_grey_24dp_img.pngic_av_play_arrow_grey_img.pngic_book_white_24dp.pngic_clear_grey_24dp_img.pngic_delete_grey_24dp.pngic_insert_photo_white_24dp.pngic_refresh_grey_24dp_img.pngic_refresh_white_24dp_img.pngic_share_grey_24dp.pngic_system_update_grey_24dp_img.pngsplash_icon.webptachiyomi_circle.png
drawable-xxhdpi
ic_av_pause_grey_24dp_img.pngic_av_play_arrow_grey_img.pngic_book_white_24dp.pngic_clear_grey_24dp_img.pngic_delete_grey_24dp.pngic_insert_photo_white_24dp.pngic_refresh_grey_24dp_img.pngic_refresh_white_24dp_img.pngic_share_grey_24dp.pngic_system_update_grey_24dp_img.pngsplash_icon.webptachiyomi_circle.png
drawable-xxxhdpi
al.pngbangumi.pngic_av_pause_grey_24dp_img.pngic_av_play_arrow_grey_img.pngic_book_white_24dp.pngic_clear_grey_24dp_img.pngic_delete_grey_24dp.pngic_insert_photo_white_24dp.pngic_refresh_grey_24dp_img.pngic_refresh_white_24dp_img.pngic_share_grey_24dp.pngic_system_update_grey_24dp_img.pngkitsu.pngmal.pngshikimori.pngsplash_icon.webptachiyomi_circle.png
drawable
branded_logo.xmlbranded_logo_icon.pngbutton_bg_transparent.xmlempty_divider.xmlfilter_mock.pngic_account_circle_black_24dp.xmlic_add_24dp.xmlic_add_to_library_24dp.xmlic_add_white_24dp.xmlic_arrow_back_24dp.xmlic_arrow_forward_24dp.xmlic_backup_24dp.xmlic_backup_black_24dp.xmlic_baseline_expand_less_24dp.xmlic_baseline_expand_more_24dp.xmlic_book_24dp.xmlic_book_black_128dp.xmlic_book_black_24dp.xmlic_bookmark_24dp.xmlic_bookmark_border_24dp.xmlic_bookmark_white_24dp.xmlic_brightness_4_24dp.xmlic_brightness_4_white_24dp.xmlic_brightness_5_24dp.xmlic_brightness_5_black_24dp.xmlic_chevron_right_white_24dp.xmlic_chrome_reader_mode_24dp.xmlic_chrome_reader_mode_black_24dp.xmlic_close_24dp.xmlic_cloud_off_24dp.xmlic_code_24dp.xmlic_code_black_24dp.xmlic_collections_bookmark_24dp.xmlic_collections_bookmark_outline_24dp.xmlic_collections_bookmark_state.xmlic_create_white_24dp.xmlic_delete_24dp.xmlic_delete_white_24dp.xmlic_done_24dp.xmlic_done_all_grey_24dp.xmlic_done_all_white_24dp.xmlic_done_outline_24dp.xmlic_done_prev_24dp.xmlic_done_white_18dp.xmlic_drag_handle_24dp.xmlic_drag_pill_24dp.xmlic_edit_24dp.xmlic_expand_more_white_24dp.xmlic_explore_24dp.xmlic_explore_black_24dp.xmlic_explore_outline_24dp.xmlic_explore_state.xmlic_extension_24dp.xmlic_extension_black_24dp.xmlic_favorite_24dp.xmlic_favorite_border_24dp.xmlic_file_download_black_128dp.xmlic_file_download_black_24dp.xmlic_file_download_white_24dp.xmlic_filter_list_24dp.xmlic_filter_list_white_24dp.xmlic_flip_to_back_24dp.xmlic_get_app_24dp.xmlic_glasses_black_128dp.xmlic_help_24dp.xmlic_help_black_24dp.xmlic_history_24dp.xmlic_image_black_24dp.xmlic_in_library_24dp.xmlic_info_24dp.xmlic_insert_photo_white_24dp.pngic_keyboard_arrow_down_black_32dp.xmlic_keyboard_arrow_up_black_32dp.xmlic_label_24dp.xmlic_label_white_24dp.xmlic_language_black_24dp.xmlic_menu_white_24dp.xmlic_more_horiz_24dp.xmlic_more_horiz_black_24dp.xmlic_more_vert_24dp.xmlic_more_vert_black_24dp.xmlic_new_releases_24dp.xmlic_new_releases_outline_24dp.xmlic_new_releases_state.xmlic_no_settings.xmlic_pause_24dp.xmlic_pause_white_24dp.xmlic_photo_24dp.xmlic_play_arrow_24dp.xmlic_play_arrow_white_24dp.xmlic_public_24dp.xmlic_push_pin_24dp.xmlic_push_pin_filled_24dp.xmlic_refresh_24dp.xmlic_refresh_white_24dp.xmlic_reorder_grey_24dp.xmlic_search_24dp.xmlic_search_white_24dp.xmlic_security_24dp.xmlic_select_all_24dp.xmlic_select_all_white_24dp.xmlic_settings_24dp.xmlic_settings_black_24dp.xmlic_settings_white_24dp.xmlic_shape_black_128dp.xmlic_share_24dp.xmlic_share_white_24dp.xmlic_skip_next_24dp.xmlic_skip_next_white_24dp.xmlic_skip_previous_24dp.xmlic_skip_previous_white_24dp.xmlic_sort_24dp.xmlic_sort_by_numeric_white_24dp.xmlic_sort_white_24dp.xmlic_sync_24dp.xmlic_sync_black_24dp.xmlic_system_update_alt_white_24dp.xmlic_tachi.xmlic_tune_24dp.xmlic_tune_black_24dp.xmlic_update_black_128dp.xmlic_update_black_24dp.xmlic_view_list_white_24dp.xmlic_view_module_24dp.xmlic_view_module_white_24dp.xmlic_watch_later_black_24dp.xmllibrary_item_selector.xmllibrary_item_selector_amoled.xmllibrary_item_selector_dark.xmllibrary_item_selector_light.xmlline_divider.xmllist_item_selector.xmllist_item_selector_amoled.xmllist_item_selector_dark.xmllist_item_selector_light.xmlmanga_info_gradient.xmlmask_star.pngmaterial_bubble_drawable.xmlmaterial_thumb_drawable.xmlrounded_rectangle.xmlsc_collections_bookmark_48dp.xmlsc_explore_48dp.xmlsc_glasses_48dp.xmlsc_history_48dp.xmlsc_new_releases_48dp.xmlsnackbar_bg.xmlsplash_background.xmltab_indicator.xmltext_button.xml
font
layout-land
layout
catalogue_drawer.xmlcatalogue_drawer_content.xmlcatalogue_global_search_controller.xmlcatalogue_global_search_controller_card.xmlcatalogue_list_item.xmlcatalogue_main_controller.xmlcatalogue_recycler_autofit.xmlcategories_controller.xmlcategories_item.xmlchangelog_header_layout.xmlchangelog_row_layout.xmlchapters_controller.xmlchapters_item.xmlcommon_action_toolbar.xmlcommon_dialog_with_checkbox.xmlcommon_listitem_dir.xmlcommon_spinner_item.xmlcommon_tabbed_sheet.xmlcommon_view_empty.xmldownload_controller.xmldownload_custom_amount.xmldownload_item.xmlextension_card_header.xmlextension_card_item.xmlextension_controller.xmlextension_detail_controller.xmlextension_detail_header.xmlglobal_search_controller.xmlglobal_search_controller_card.xmlglobal_search_controller_card_item.xmlhistory_controller.xmlhistory_item.xmllibrary_category.xmllibrary_controller.xmllibrary_drawer.xmllibrary_grid_recycler.xmllibrary_list_recycler.xmlmain_activity.xmlmanga_chapters_header.xmlmanga_controller.xmlmanga_info_controller.xmlmanga_info_header.xmlmanga_info_web_controller.xmlmaterial_fastscroll.xmlmigration_controller.xmlmigration_manga_controller.xmlmigration_sources_controller.xmlnavigation_header.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.xmlnavigation_view_text.xmlpager_controller.xmlpref_account_login.xmlpref_item_source.xmlpref_library_columns.xmlpref_more_header.xmlpref_settings.xmlpref_widget_imageview.xmlreader_activity.xmlreader_color_filter.xmlreader_color_filter_sheet.xmlreader_page_sheet.xmlreader_settings_sheet.xmlrecent_chapters_controller.xmlrecent_section_item.xmlrecently_read_controller.xmlrecently_read_item.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_controller.xmlsource_filter_sheet.xmlsource_list_item.xmlsource_main_controller.xmlsource_main_controller_card_header.xmlsource_main_controller_card_item.xmlsource_preferences_controller.xmlsource_progress_item.xmlsource_recycler_autofit.xmltrack_chapters_dialog.xmltrack_controller.xmltrack_item.xmltrack_score_dialog.xmltrack_search_dialog.xmltrack_search_item.xmlupdates_controller.xmlupdates_item.xmlwebview_activity.xml
menu
bottom_nav.xmlcatalogue_list.xmlcatalogue_main.xmlcatalogue_new_list.xmlcategory_selection.xmlchapter_recent.xmlchapter_recent_selection.xmlchapter_selection.xmlchapter_single.xmlchapters.xmldownload_queue.xmldownload_single.xmlextension_details.xmlextension_main.xmlgeneric_selection.xmlglobal_search.xmllibrary.xmllibrary_selection.xmlmanga.xmlmanga_info.xmlmenu_navigation.xmlmigration.xmlreader.xmlsource_browse.xmlsource_main.xmlupdates.xmlupdates_chapter_selection.xmlweb_view.xmlwebview.xml
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
raw
values-ar
values-b+es+419
values-be
values-bg
values-bn
values-ca
values-cs
values-cv
values-de
values-el
values-es
values-fa
values-fi
values-fil
values-fr
values-he
values-hi
values-hr
values-hu
values-in
values-it
values-ja
values-ka-rGE
values-kn
values-ko
values-land
values-lv
values-mr
values-ms
values-nb-rNO
values-night
values-nl
values-pl
values-pt-rBR
values-pt
values-ro
values-ru
values-sc
values-sk
values-sr
values-sv
values-sw600dp
values-th
values-tl
values-tr
values-uk
values-ur-rPK
values-v21
values-v23
values-v26
values-v27
values-vi
values-zh-rCN
values-zh-rTW
values
xml
standard
test
java
eu
kanade
tachiyomi
buildSrc
gradle.propertiesgradle/wrapper
gradlewgradlew.batktlintCodeStyle.xmlsettings.gradlesettings.gradle.kts
10
.github/CONTRIBUTING.md
vendored
10
.github/CONTRIBUTING.md
vendored
@@ -1,10 +1,10 @@
|
||||
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/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)
|
||||
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
@@ -14,9 +14,9 @@
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* 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)
|
||||
@@ -29,5 +29,5 @@ 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"
|
||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
|
29
.github/ISSUE_TEMPLATE.md
vendored
29
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,17 +1,26 @@
|
||||
**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.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
**Android version:**
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
**Issue/Request:**
|
||||
---
|
||||
|
||||
**Steps to reproduce (if applicable)**
|
||||
### Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
## Steps to reproduce
|
||||
1. First step
|
||||
2. Second step
|
||||
|
||||
**Other details:**
|
||||
## Issue/Request
|
||||
?
|
||||
|
||||
## Other details
|
||||
Additional details and attachments.
|
||||
|
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,14 +1,26 @@
|
||||
---
|
||||
name: "🐞 Bug report"
|
||||
about: Report a bug
|
||||
title: "[Bug] Write short description here"
|
||||
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.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
### Device information
|
||||
* Tachiyomi version: ?
|
||||
* Android version: ?
|
||||
* Device: ?
|
||||
|
||||
## Steps to reproduce
|
||||
1. First step
|
||||
|
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,10 +1,22 @@
|
||||
---
|
||||
name: "🌟 Feature request"
|
||||
about: Suggest a feature to improve Tachiyomi
|
||||
title: "[Feature Request] Write short description here"
|
||||
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.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
### Why/User Benefit/User Problem
|
||||
(explain why this feature should be added)
|
||||
|
||||
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: "Extension/source/catalogue issue"
|
||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||
labels: "catalog"
|
||||
---
|
||||
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before ![]() (image error) Size: 1.0 MiB After ![]() (image error) Size: 1.2 MiB ![]() ![]() |
34
.github/workflows/issue_closer.yml
vendored
Normal file
34
.github/workflows/issue_closer.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Issue closer
|
||||
on: [issues]
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose when created in wrong repo
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: title
|
||||
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
|
||||
- name: Autoclose when no short description provided
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: title
|
||||
regex: ".*<Write short description here>*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
||||
- name: Autoclose when body acknowledgement section not removed
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: body
|
||||
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
|
||||
- name: Autoclose when body requested information not filled out
|
||||
uses: arkon/issue-closer-action@v1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
type: body
|
||||
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
|
||||
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -2,8 +2,15 @@
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
.DS_Store
|
||||
/build
|
||||
.idea/
|
||||
*iml
|
||||
*.iml
|
||||
*/build
|
||||
|
||||
# Built files
|
||||
*/build
|
||||
/build
|
||||
*.apk
|
||||
app/**/output.json
|
||||
|
||||
# Hebrew assets are copied on build
|
||||
app/src/main/res/values-iw/
|
||||
|
108
.travis.yml
108
.travis.yml
@@ -1,59 +1,81 @@
|
||||
dist: trusty
|
||||
language: android
|
||||
|
||||
android:
|
||||
components:
|
||||
- build-tools-29.0.2
|
||||
- android-28
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-29.0.3
|
||||
- android-29
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-android-support
|
||||
- extra-google-google_play_services
|
||||
|
||||
licenses:
|
||||
- android-sdk-license-.+
|
||||
- 'android-sdk-license-.+'
|
||||
- 'android-sdk-preview-license-.+'
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
|
||||
tar xf secrets.tar;
|
||||
mv debug.keystore "$HOME/.android";
|
||||
fi
|
||||
- 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"
|
||||
fi
|
||||
- mkdir "$ANDROID_HOME/licenses" || true
|
||||
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
|
||||
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
|
||||
|
||||
install:
|
||||
- echo y | sdkmanager "ndk-bundle"
|
||||
|
||||
before_script:
|
||||
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
|
||||
|
||||
script: ".travis/build.sh"
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
- 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"
|
||||
- "$HOME/.gradle/caches/"
|
||||
- "$HOME/.gradle/wrapper/"
|
||||
- "$HOME/.android/build-cache"
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||
file: tachiyomi-v*.apk
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: script
|
||||
script: ".travis/deploy.sh"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
condition: "-z $TRAVIS_TAG"
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
|
||||
file: tachiyomi-v*.apk
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: script
|
||||
script: ".travis/deploy.sh"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
condition: "-z $TRAVIS_TAG"
|
||||
repo: inorichi/tachiyomi
|
||||
- provider: script
|
||||
script: ".travis/deploy.sh"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: dev
|
||||
condition: "-z $TRAVIS_TAG"
|
||||
repo: inorichi/tachiyomi
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
|
||||
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
|
||||
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
|
||||
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
|
||||
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
|
||||
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
|
||||
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=
|
||||
- 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=
|
||||
|
@@ -2,8 +2,6 @@
|
||||
|
||||
git fetch --unshallow #required for commit count
|
||||
|
||||
cp .travis/google-services.json app/
|
||||
|
||||
if [ -z "$TRAVIS_TAG" ]; then
|
||||
./gradlew clean assembleStandardDebug
|
||||
|
||||
|
28
PREVIEW_RELEASE_NOTES.md
Normal file
28
PREVIEW_RELEASE_NOTES.md
Normal file
@@ -0,0 +1,28 @@
|
||||
### r1810
|
||||
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
|
||||
run properly. This includes app updates, library updates, and automatic backups.
|
||||
|
||||
### r1340
|
||||
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
|
||||
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
|
||||
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
|
||||
don't store anything.
|
||||
|
||||
### r959
|
||||
- The download manager has been rewritten and it's possible some of your downloads
|
||||
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
|
||||
- You can now download to any folder in your SD card.
|
||||
- The download directory setting has been reset.
|
||||
|
||||
### r857
|
||||
- **Important!** Delete after read has been updated.
|
||||
This means the value has been reset set to disabled.
|
||||
This can be changed in Settings > Downloads
|
||||
|
||||
### r736
|
||||
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
|
||||
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
|
||||
|
||||
### r724
|
||||
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
|
||||
from the info tab, or clearing the database (the latter won't fix covers from library manga).
|
20
README.md
20
README.md
@@ -1,29 +1,29 @@
|
||||
| Build | Stable | Dev | Contribute | Support Server |
|
||||
| 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://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) |
|
||||
|
||||
|
||||
# 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, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/explore/anime), and [Shikimori](https://shikimori.one) support
|
||||
* [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
|
||||
* Create backups locally to read offline or to your desired cloud service
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
|
||||
|
||||
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest).
|
||||
If you want to try new features before they get to the stable release, you can download the preview version [here](http://tachiyomi.kanade.eu/latest).
|
||||
|
||||
## Issues, Feature Requests and Contributing
|
||||
|
||||
@@ -38,9 +38,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<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)
|
||||
@@ -55,7 +55,7 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
|
||||
<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.
|
||||
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -2,4 +2,3 @@
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
google-services.json
|
||||
|
210
app/build.gradle
210
app/build.gradle
@@ -1,6 +1,9 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
@@ -29,25 +32,22 @@ ext {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '29.0.2'
|
||||
publishNonDefault true
|
||||
compileSdkVersion AndroidConfig.compileSdk
|
||||
buildToolsVersion AndroidConfig.buildTools
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 42
|
||||
versionName "0.8.5"
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 46
|
||||
versionName "0.10.0"
|
||||
|
||||
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 {
|
||||
@@ -55,11 +55,24 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix "-${getCommitCount()}"
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
postprocessing {
|
||||
obfuscate false
|
||||
optimizeCode true
|
||||
removeUnusedCode false
|
||||
removeUnusedResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
@@ -86,87 +99,108 @@ android {
|
||||
exclude 'META-INF/NOTICE'
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
disable 'ExtraTranslation'
|
||||
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Modified dependencies
|
||||
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
// AndroidX libraries
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'androidx.webkit:webkit:1.3.0-rc01'
|
||||
|
||||
// Android support library
|
||||
final support_library_version = '28.0.0'
|
||||
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"
|
||||
final lifecycle_version = '2.3.0-alpha06'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
// Job scheduling
|
||||
final work_version = '2.4.0'
|
||||
implementation "androidx.work:work-runtime:$work_version"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
// UI library
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:11.8.0'
|
||||
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
|
||||
|
||||
// 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.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||
|
||||
// Network client
|
||||
implementation "com.squareup.okhttp3:okhttp:3.10.0"
|
||||
implementation 'com.squareup.okio:okio:1.14.0'
|
||||
final okhttp_version = '4.8.0'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||
implementation 'com.squareup.okio:okio:2.7.0'
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation 'org.conscrypt:conscrypt-android:2.4.0'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.3.0'
|
||||
final retrofit_version = '2.9.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// JavaScript engine
|
||||
implementation 'com.squareup.duktape:duktape-android:1.2.0'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
|
||||
// Disk
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||
implementation 'com.github.inorichi:junrar-android:634c1f5'
|
||||
|
||||
// HTML parser
|
||||
implementation 'org.jsoup:jsoup:1.12.1'
|
||||
|
||||
// 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'
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
// Database
|
||||
implementation 'android.arch.persistence:db:1.1.1'
|
||||
implementation 'androidx.sqlite:sqlite:2.1.0'
|
||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||
implementation 'io.requery:sqlite-android:3.25.2'
|
||||
implementation 'io.requery:sqlite-android:3.32.2'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
@@ -177,52 +211,62 @@ dependencies {
|
||||
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||
|
||||
// Image library
|
||||
final glide_version = '4.6.1'
|
||||
final glide_version = '4.11.0'
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
// Transformations
|
||||
implementation 'jp.wasabeef:glide-transformations:3.1.1'
|
||||
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Crash reports
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
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.0.0-rc4'
|
||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
|
||||
implementation 'eu.davidea:flexible-adapter:5.1.0'
|
||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'com.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'
|
||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
||||
implementation 'com.github.mthli:Slice:v1.3'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
final material_dialogs_version = '3.1.1'
|
||||
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
|
||||
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
|
||||
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
|
||||
|
||||
// Conductor
|
||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||
implementation ("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
|
||||
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||
|
||||
// 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"
|
||||
// FlowBinding
|
||||
final flowbinding_version = '0.12.0'
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||
|
||||
// Licenses
|
||||
final aboutlibraries_version = '8.3.0'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:1.7.1'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.assertj:assertj-core:3.12.2'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
@@ -230,14 +274,23 @@ dependencies {
|
||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
final coroutines_version = '1.3.3'
|
||||
// Do not update until we bump to Kotlin 1.4, see https://github.com/Kotlin/kotlinx.coroutines/issues/2049
|
||||
final coroutines_version = '1.3.6'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
|
||||
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.61'
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@@ -251,14 +304,19 @@ repositories {
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
|
||||
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||
tasks.withType(AbstractKotlinCompile).all {
|
||||
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
task copyResources(type: Copy) {
|
||||
from './src/main/res/values-he'
|
||||
into './src/main/res/values-iw'
|
||||
include '**/*'
|
||||
}
|
||||
|
||||
preBuild.dependsOn(ktlintFormat, copyResources)
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
51
app/proguard-rules.pro
vendored
51
app/proguard-rules.pro
vendored
@@ -1,19 +1,27 @@
|
||||
-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.** { *; }
|
||||
|
||||
# Design library
|
||||
-dontwarn com.google.android.material.**
|
||||
-keep class com.google.android.material.** { *; }
|
||||
-keep interface com.google.android.material.** { *; }
|
||||
-keep public class com.google.android.material.R$* { *; }
|
||||
|
||||
-keep class com.hippo.image.** { *; }
|
||||
-keep interface com.hippo.image.** { *; }
|
||||
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||
<init>();
|
||||
}
|
||||
|
||||
# OkHttp
|
||||
-dontwarn okhttp3.**
|
||||
@@ -46,26 +54,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.**
|
||||
|
||||
@@ -83,10 +71,3 @@
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-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.** { *; }
|
@@ -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" />
|
||||
|
@@ -8,50 +8,71 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:hardwareAccelerated="true"
|
||||
android:usesCleartextTraffic="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.Tachiyomi.Light"
|
||||
android:usesCleartextTraffic="true">
|
||||
<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">
|
||||
<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"/>
|
||||
|
||||
<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" />
|
||||
<activity
|
||||
android:name=".ui.security.BiometricUnlockActivity"
|
||||
android:theme="@style/Theme.Splash" />
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme" />
|
||||
<activity
|
||||
android:name=".ui.setting.AnilistLoginActivity"
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -65,7 +86,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.ShikimoriLoginActivity"
|
||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -79,7 +100,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.BangumiLoginActivity"
|
||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||
android:label="Bangumi">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -95,10 +116,10 @@
|
||||
|
||||
<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">
|
||||
@@ -125,11 +146,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>
|
||||
|
||||
|
Binary file not shown.
@@ -3,41 +3,65 @@ 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 java.security.Security
|
||||
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.injectLazy
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
|
||||
@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 = "https://tachiyomi.kanade.eu/crash_report",
|
||||
httpMethod = HttpSender.Method.PUT
|
||||
)
|
||||
open class App : Application(), LifecycleObserver {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// SoLoader.init(this, false)
|
||||
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||
// val client = AndroidFlipperClient.getInstance(this)
|
||||
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||
// client.start()
|
||||
// }
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
Injekt = InjektScope(DefaultRegistrar())
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
setupNotificationChannels()
|
||||
|
||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@@ -50,27 +74,20 @@ 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() {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
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() {
|
||||
ACRA.init(this)
|
||||
}
|
||||
|
||||
protected open fun setupNotificationChannels() {
|
||||
Notifications.createChannels(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -11,14 +11,17 @@ 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.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
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) }
|
||||
@@ -43,20 +46,14 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
|
||||
rxAsync { get<PreferencesHelper>() }
|
||||
GlobalScope.launch { get<PreferencesHelper>() }
|
||||
|
||||
rxAsync { get<NetworkHelper>() }
|
||||
GlobalScope.launch { get<NetworkHelper>() }
|
||||
|
||||
rxAsync { get<SourceManager>() }
|
||||
GlobalScope.launch { get<SourceManager>() }
|
||||
|
||||
rxAsync { get<DatabaseHelper>() }
|
||||
|
||||
rxAsync { get<DownloadManager>() }
|
||||
GlobalScope.launch { get<DatabaseHelper>() }
|
||||
|
||||
GlobalScope.launch { get<DownloadManager>() }
|
||||
}
|
||||
|
||||
private fun rxAsync(block: () -> Unit) {
|
||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
@@ -16,18 +18,33 @@ object Migrations {
|
||||
*/
|
||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||
val context = preferences.context
|
||||
val oldVersion = preferences.lastVersionCode().getOrDefault()
|
||||
val oldVersion = preferences.lastVersionCode().get()
|
||||
|
||||
// Cancel app updater job for debug builds that don't include it
|
||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
||||
UpdaterJob.cancelTask(context)
|
||||
}
|
||||
|
||||
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 +56,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 +72,25 @@ 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
|
||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,23 +1,10 @@
|
||||
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"
|
||||
}
|
||||
|
@@ -1,25 +1,24 @@
|
||||
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.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
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 +30,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 +46,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) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
} else {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var backupManager: BackupManager
|
||||
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)
|
||||
backupManager = BackupManager(this)
|
||||
|
||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
||||
stopSelf(startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
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,51 @@
|
||||
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.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
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 {
|
||||
backupManager.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,16 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
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.BackupCreateService.Companion.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
||||
@@ -18,40 +24,43 @@ import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.*
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import kotlin.math.max
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
internal val databaseHelper: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
internal val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Tracking manager
|
||||
*/
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Version of parser
|
||||
@@ -64,11 +73,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
var parser: Gson = initParser()
|
||||
|
||||
/**
|
||||
* Preferences
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Set version of parser
|
||||
*
|
||||
@@ -81,7 +85,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
private fun initParser(): Gson = when (version) {
|
||||
1 -> GsonBuilder().create()
|
||||
2 -> GsonBuilder()
|
||||
2 ->
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
@@ -97,7 +102,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) {
|
||||
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
||||
// Create root object
|
||||
val root = JsonObject()
|
||||
|
||||
@@ -107,24 +112,40 @@ 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 {
|
||||
@@ -138,42 +159,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
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() }
|
||||
.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")
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
|
||||
newFile.openOutputStream().bufferedWriter().use {
|
||||
parser.toJson(root, it)
|
||||
}
|
||||
|
||||
return newFile.uri.toString()
|
||||
} else {
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
?: throw Exception("Couldn't create backup file")
|
||||
?: 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)
|
||||
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 +221,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 +233,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 +242,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 +250,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) }
|
||||
@@ -264,13 +281,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
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
|
||||
}
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,13 +299,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*/
|
||||
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)
|
||||
}
|
||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||
.doOnNext { pair ->
|
||||
if (pair.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +325,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 +350,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 = mutableListOf<MangaCategory>()
|
||||
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 +361,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 +374,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 = mutableListOf<History>()
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory.apply {
|
||||
last_read = Math.max(lastRead, dbHistory.last_read)
|
||||
last_read = max(lastRead, dbHistory.last_read)
|
||||
}
|
||||
historyToBeUpdated.add(dbHistory)
|
||||
} else {
|
||||
@@ -393,9 +408,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = ArrayList<Track>()
|
||||
val trackToUpdate = mutableListOf<Track>()
|
||||
|
||||
for (track in tracks) {
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
var isInDatabase = false
|
||||
@@ -408,7 +423,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 +437,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
}
|
||||
}
|
||||
// Update database
|
||||
if (!trackToUpdate.isEmpty()) {
|
||||
if (trackToUpdate.isNotEmpty()) {
|
||||
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
@@ -438,8 +453,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)
|
||||
@@ -464,7 +480,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
internal fun getMangaFromDatabase(manga: Manga): Manga? =
|
||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Returns list containing manga from library
|
||||
@@ -472,7 +488,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @return [Manga] from library
|
||||
*/
|
||||
internal fun getFavoriteManga(): List<Manga> =
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
@@ -480,7 +496,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
internal fun insertManga(manga: Manga): Long? =
|
||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
@@ -494,5 +510,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
*
|
||||
* @return number of backups selected by user
|
||||
*/
|
||||
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
|
||||
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
|
||||
}
|
||||
|
@@ -0,0 +1,161 @@
|
||||
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 java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class BackupNotifier(private val context: Context) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
}
|
||||
|
||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setAutoCancel(false)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.show(id: Int) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
fun showBackupProgress(): NotificationCompat.Builder {
|
||||
val builder = with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.creating_backup))
|
||||
|
||||
setProgress(0, 0, true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun showBackupError(error: String?) {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.creating_backup_error))
|
||||
setContentText(error)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
fun showBackupComplete(unifile: UniFile) {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.backup_created))
|
||||
|
||||
if (unifile.filePath != null) {
|
||||
setContentText(unifile.filePath)
|
||||
}
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
|
||||
val builder = with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.restoring_backup))
|
||||
|
||||
if (!preferences.hideNotificationContent()) {
|
||||
setContentText(content)
|
||||
}
|
||||
|
||||
setProgress(maxAmount, progress, false)
|
||||
setOnlyAlertOnce(true)
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_stop),
|
||||
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
|
||||
)
|
||||
}
|
||||
|
||||
builder.show(Notifications.ID_RESTORE_PROGRESS)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun showRestoreError(error: String?) {
|
||||
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.restoring_backup_error))
|
||||
setContentText(error)
|
||||
|
||||
show(Notifications.ID_RESTORE_COMPLETE)
|
||||
}
|
||||
}
|
||||
|
||||
fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) {
|
||||
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||
|
||||
val timeString = context.getString(
|
||||
R.string.restore_duration,
|
||||
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
|
||||
TimeUnit.MILLISECONDS.toMinutes(time)
|
||||
)
|
||||
)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.restore_completed))
|
||||
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
|
||||
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||
val destFile = File(path, file)
|
||||
val uri = destFile.getUriCompat(context)
|
||||
|
||||
addAction(
|
||||
R.drawable.nnf_ic_file_folder,
|
||||
context.getString(R.string.action_open_log),
|
||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||
)
|
||||
}
|
||||
|
||||
show(Notifications.ID_RESTORE_COMPLETE)
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,10 +4,13 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
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
|
||||
@@ -20,26 +23,31 @@ 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.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.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 timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
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
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Restores backup from json file
|
||||
* Restores backup from a JSON file.
|
||||
*/
|
||||
class BackupRestoreService : Service() {
|
||||
|
||||
@@ -51,8 +59,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
|
||||
@@ -65,7 +73,11 @@ class BackupRestoreService : Service() {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
}
|
||||
context.startService(intent)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
} else {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +88,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,10 +98,7 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscription where the update is done.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
private var job: Job? = null
|
||||
|
||||
/**
|
||||
* The progress of a backup restore
|
||||
@@ -99,51 +110,47 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||
|
||||
/**
|
||||
* List containing errors
|
||||
*/
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
/**
|
||||
* Backup manager
|
||||
*/
|
||||
private lateinit var backupManager: BackupManager
|
||||
private lateinit var notifier: BackupNotifier
|
||||
|
||||
/**
|
||||
* Database
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Tracking manager
|
||||
*/
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
private 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()
|
||||
|
||||
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() {
|
||||
job?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,153 +167,151 @@ 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 uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
||||
// Cancel any previous job if needed.
|
||||
job?.cancel()
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
writeErrorLog()
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
notifier.showRestoreError(exception.message)
|
||||
|
||||
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()
|
||||
stopSelf(startId)
|
||||
}
|
||||
job = GlobalScope.launch(handler) {
|
||||
if (!restoreBackup(uri)) {
|
||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||
}
|
||||
}
|
||||
job?.invokeOnCompletion {
|
||||
stopSelf(startId)
|
||||
}
|
||||
|
||||
return Service.START_NOT_STICKY
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Observable] containing restore process.
|
||||
* Restores data from backup file.
|
||||
*
|
||||
* @param uri restore file
|
||||
* @return [Observable<Manga>]
|
||||
* @param uri backup file to restore
|
||||
*/
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
private fun restoreBackup(uri: Uri): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
return Observable.just(Unit)
|
||||
.map {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
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)
|
||||
}
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let { restoreCategories(it) }
|
||||
|
||||
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())
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
|
||||
|
||||
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)
|
||||
// Restore individual manga
|
||||
mangasJson.forEach {
|
||||
if (job?.isActive != true) {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
.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() }
|
||||
restoreManga(it.asJsonObject)
|
||||
}
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
|
||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||
db.inTransaction {
|
||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||
}
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||
}
|
||||
|
||||
private fun restoreManga(mangaJson: JsonObject) {
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
|
||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
||||
mangaJson.get(CHAPTERS)
|
||||
?: JsonArray()
|
||||
)
|
||||
val categories = backupManager.parser.fromJson<List<String>>(
|
||||
mangaJson.get(CATEGORIES)
|
||||
?: JsonArray()
|
||||
)
|
||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
||||
mangaJson.get(HISTORY)
|
||||
?: JsonArray()
|
||||
)
|
||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
||||
mangaJson.get(TRACK)
|
||||
?: JsonArray()
|
||||
)
|
||||
|
||||
try {
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
return File("")
|
||||
|
||||
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
|
||||
* @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)
|
||||
private fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,59 +322,58 @@ class BackupRestoreService : Service() {
|
||||
* @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 restoreMangaFetch(
|
||||
source: Source,
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
backupManager.restoreMangaFetchObservable(source, manga)
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap {
|
||||
trackingFetchObservable(it, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun 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 restoreMangaNoFetch(
|
||||
source: Source,
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<String>,
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
Observable.just(backupManga)
|
||||
.flatMap { manga ->
|
||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.map { manga }
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap { manga ->
|
||||
trackingFetchObservable(manga, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||
@@ -392,11 +396,11 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
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())
|
||||
}
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,20 +411,20 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
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()
|
||||
}
|
||||
.flatMap { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
track
|
||||
}
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||
Observable.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -430,16 +434,33 @@ class BackupRestoreService : Service() {
|
||||
* @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)
|
||||
private fun showRestoreProgress(
|
||||
progress: Int,
|
||||
amount: Int,
|
||||
title: String
|
||||
) {
|
||||
notifier.showRestoreProgress(title, progress, amount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write errors to error log
|
||||
*/
|
||||
private fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
|
||||
object BackupRestoreValidator {
|
||||
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
*
|
||||
* @throws Exception if version or manga cannot be found.
|
||||
* @return List of required sources.
|
||||
*/
|
||||
fun validate(context: Context, uri: Uri): Map<Long, String> {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
|
||||
val version = json.get(Backup.VERSION)
|
||||
val mangasJson = json.get(Backup.MANGAS)
|
||||
if (version == null || mangasJson == null) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||
}
|
||||
|
||||
if (mangasJson.asJsonArray.size() == 0) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
return getSourceMapping(json)
|
||||
}
|
||||
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||
|
||||
return extensionsMapping.asJsonArray
|
||||
.map {
|
||||
val items = it.asString.split(":")
|
||||
items[0].toLong() to items[1]
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.backup.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"
|
||||
|
||||
@@ -20,4 +22,4 @@ object Backup {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||
return "tachiyomi_$date.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
data class DHistory(val url: String,val lastRead: Long)
|
||||
data class DHistory(val url: String, val lastRead: Long)
|
||||
|
@@ -28,4 +28,4 @@ object CategoryTypeAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -58,4 +56,4 @@ object ChapterTypeAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -29,4 +29,4 @@ object HistoryTypeAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,4 +34,4 @@ object MangaTypeAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.backup.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()
|
||||
@@ -59,4 +56,4 @@ object TrackTypeAdapter {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,14 +7,15 @@ 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 okhttp3.Response
|
||||
import okio.Okio
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import okhttp3.Response
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class used to create chapter cache
|
||||
@@ -38,17 +39,19 @@ 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()
|
||||
|
||||
/** 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.
|
||||
@@ -76,16 +79,17 @@ class ChapterCache(private val context: Context) {
|
||||
*/
|
||||
fun removeFileFromCache(file: String): Boolean {
|
||||
// Make sure we don't delete the journal file (keeps track of cache).
|
||||
if (file == "journal" || file.startsWith("journal."))
|
||||
if (file == "journal" || file.startsWith("journal.")) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
return 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)
|
||||
diskCache.remove(key)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +130,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 +138,6 @@ class ChapterCache(private val context: Context) {
|
||||
diskCache.flush()
|
||||
editor.commit()
|
||||
editor.abortUnlessCommitted()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// Ignore.
|
||||
} finally {
|
||||
@@ -149,10 +152,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +173,7 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Add image to cache.
|
||||
*
|
||||
*
|
||||
* @param imageUrl url of image.
|
||||
* @param response http response from page.
|
||||
* @throws IOException image error.
|
||||
@@ -186,12 +189,12 @@ 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()
|
||||
}
|
||||
}
|
||||
@@ -200,4 +203,3 @@ class ChapterCache(private val context: Context) {
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -18,22 +18,22 @@ 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)
|
||||
@@ -56,8 +56,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()
|
||||
}
|
||||
|
@@ -26,22 +26,22 @@ 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)
|
||||
@@ -80,9 +80,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()
|
||||
}
|
||||
|
||||
|
@@ -18,22 +18,22 @@ 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)
|
||||
@@ -56,8 +56,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()
|
||||
}
|
||||
|
@@ -16,22 +16,22 @@ 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)
|
||||
@@ -52,8 +52,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()
|
||||
}
|
||||
|
@@ -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,24 +31,24 @@ 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 {
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
@@ -62,6 +64,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
put(COL_INITIALIZED, obj.initialized)
|
||||
put(COL_VIEWER, obj.viewer)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||
put(COL_DATE_ADDED, obj.date_added)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +86,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 +101,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()
|
||||
}
|
||||
|
@@ -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,22 +27,22 @@ 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)
|
||||
@@ -54,7 +56,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_TRACKING_URL, obj.tracking_url)
|
||||
put(COL_SCORE, obj.score)
|
||||
|
||||
put(COL_START_DATE, obj.started_reading_date)
|
||||
put(COL_FINISH_DATE, obj.finished_reading_date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,14 +75,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
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -12,22 +12,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 +80,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 +97,4 @@ interface Manga : SManga {
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -11,47 +11,65 @@ 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 +80,22 @@ 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 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()
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
interface HistoryQueries : DbProvider {
|
||||
|
||||
@@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider {
|
||||
* @param date recent date range
|
||||
*/
|
||||
fun getRecentManga(date: Date) = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getRecentMangasQuery())
|
||||
.args(date.time)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getRecentMangasQuery())
|
||||
.args(date.time)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(History::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getHistoryByMangaId())
|
||||
.args(mangaId)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(History::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getHistoryByMangaId())
|
||||
.args(mangaId)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
|
||||
.`object`(History::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getHistoryByChapterUrl())
|
||||
.args(chapterUrl)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.`object`(History::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getHistoryByChapterUrl())
|
||||
.args(chapterUrl)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
@@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider {
|
||||
* @param history history object
|
||||
*/
|
||||
fun updateHistoryLastRead(history: History) = db.put()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
@@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider {
|
||||
* @param historyList history object list
|
||||
*/
|
||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteHistory() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteHistoryNoLastRead() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_LAST_READ} = ?")
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider {
|
||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||
|
||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaCategoryTable.TABLE)
|
||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||
db.inTransaction {
|
||||
@@ -28,5 +30,4 @@ interface MangaCategoryQueries : DbProvider {
|
||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,13 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.*
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
@@ -15,104 +21,142 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
interface MangaQueries : DbProvider {
|
||||
|
||||
fun getMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getLibraryMangas() = db.get()
|
||||
.listOfObjects(LibraryManga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
.build())
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
.listOfObjects(LibraryManga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(libraryQuery)
|
||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COL_TITLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(1)
|
||||
.orderBy(MangaTable.COL_TITLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getManga(url: String, sourceId: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build())
|
||||
.prepare()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
||||
.whereArgs(url, sourceId)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getManga(id: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build())
|
||||
.prepare()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun updateFlags(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.prepare()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||
.prepare()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaFavorite(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFavoritePutResolver())
|
||||
.prepare()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFavoritePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaViewer(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
.prepare()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaCoverLastModified(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangasNotInLibrary() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(0)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(0)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun deleteMangas() = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getLastReadManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
|
||||
fun getTotalChapterManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getTotalChapterMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getLatestChapterManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getLatestChapterMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
*/
|
||||
val libraryQuery = """
|
||||
val libraryQuery =
|
||||
"""
|
||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||
FROM (
|
||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
||||
@@ -33,10 +34,13 @@ val libraryQuery = """
|
||||
/**
|
||||
* Query to get the recent chapters of manga from the library up to a date.
|
||||
*/
|
||||
fun getRecentsQuery() = """
|
||||
fun getRecentsQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||
WHERE ${Manga.COL_FAVORITE} = 1
|
||||
AND ${Chapter.COL_DATE_UPLOAD} > ?
|
||||
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
|
||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||
"""
|
||||
|
||||
@@ -47,7 +51,8 @@ fun getRecentsQuery() = """
|
||||
* and are read after the given time period
|
||||
* @return return limit is 25
|
||||
*/
|
||||
fun getRecentMangasQuery() = """
|
||||
fun getRecentMangasQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
@@ -65,7 +70,8 @@ fun getRecentMangasQuery() = """
|
||||
LIMIT 25
|
||||
"""
|
||||
|
||||
fun getHistoryByMangaId() = """
|
||||
fun getHistoryByMangaId() =
|
||||
"""
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
@@ -73,7 +79,8 @@ fun getHistoryByMangaId() = """
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getHistoryByChapterUrl() = """
|
||||
fun getHistoryByChapterUrl() =
|
||||
"""
|
||||
SELECT ${History.TABLE}.*
|
||||
FROM ${History.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
@@ -81,7 +88,8 @@ fun getHistoryByChapterUrl() = """
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getLastReadMangaQuery() = """
|
||||
fun getLastReadMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
@@ -93,7 +101,8 @@ fun getLastReadMangaQuery() = """
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getTotalChapterMangaQuery()= """
|
||||
fun getTotalChapterMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
@@ -102,12 +111,23 @@ fun getTotalChapterMangaQuery()= """
|
||||
ORDER by COUNT(*)
|
||||
"""
|
||||
|
||||
fun getLatestChapterMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by max DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
fun getCategoriesForMangaQuery() = """
|
||||
fun getCategoriesForMangaQuery() =
|
||||
"""
|
||||
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||
"""
|
||||
"""
|
||||
|
@@ -11,24 +11,27 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks(manga: Manga) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build())
|
||||
.prepare()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
|
||||
|
||||
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
|
||||
|
||||
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
|
||||
.byQuery(DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
}
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
|
||||
.whereArgs(manga.id, sync.id)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
}
|
||||
|
@@ -20,16 +20,14 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(chapter.url)
|
||||
.build()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ?")
|
||||
.whereArgs(chapter.url)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -20,16 +20,14 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(chapter.id)
|
||||
.build()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(chapter.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -20,13 +20,12 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(chapter.url, chapter.manga_id)
|
||||
.build()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
||||
.whereArgs(chapter.url, chapter.manga_id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.support.annotation.NonNull
|
||||
import androidx.annotation.NonNull
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
@@ -19,25 +19,25 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(history)
|
||||
|
||||
val cursor = db.lowLevel().query(Query.builder()
|
||||
val cursor = db.lowLevel().query(
|
||||
Query.builder()
|
||||
.table(updateQuery.table())
|
||||
.where(updateQuery.where())
|
||||
.whereArgs(updateQuery.whereArgs())
|
||||
.build())
|
||||
.build()
|
||||
)
|
||||
|
||||
val putResult: PutResult
|
||||
|
||||
try {
|
||||
if (cursor.count == 0) {
|
||||
putResult = cursor.use { putCursor ->
|
||||
if (putCursor.count == 0) {
|
||||
val insertQuery = mapToInsertQuery(history)
|
||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||
PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||
} else {
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
||||
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
} finally {
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
putResult
|
||||
@@ -48,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
* @param obj history object
|
||||
*/
|
||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||
.whereArgs(obj.chapter_id)
|
||||
.build()
|
||||
.table(HistoryTable.TABLE)
|
||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||
.whereArgs(obj.chapter_id)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Create content query
|
||||
@@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||
|
||||
return MangaChapter(manga, chapter)
|
||||
}
|
||||
|
||||
}
|
||||
|
31
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt
Normal file
31
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
|
||||
}
|
||||
}
|
@@ -20,14 +20,12 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -20,14 +20,12 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -20,14 +20,12 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -20,13 +20,12 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -20,13 +20,12 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -13,11 +13,11 @@ object CategoryTable {
|
||||
const val COL_FLAGS = "flags"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_NAME TEXT NOT NULL,
|
||||
$COL_ORDER INTEGER NOT NULL,
|
||||
$COL_FLAGS INTEGER NOT NULL
|
||||
)"""
|
||||
|
||||
}
|
||||
|
@@ -29,7 +29,8 @@ object ChapterTable {
|
||||
const val COL_SOURCE_ORDER = "source_order"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
@@ -51,7 +52,7 @@ object ChapterTable {
|
||||
|
||||
val createUnreadChaptersIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
||||
"WHERE $COL_READ = 0"
|
||||
"WHERE $COL_READ = 0"
|
||||
|
||||
val sourceOrderUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
||||
@@ -61,5 +62,4 @@ object ChapterTable {
|
||||
|
||||
val addScanlator: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||
|
||||
}
|
||||
|
@@ -31,7 +31,8 @@ object HistoryTable {
|
||||
* query to create history table
|
||||
*/
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
||||
$COL_LAST_READ LONG,
|
||||
|
@@ -11,7 +11,8 @@ object MangaCategoryTable {
|
||||
const val COL_CATEGORY_ID = "category_id"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_CATEGORY_ID INTEGER NOT NULL,
|
||||
@@ -20,5 +21,4 @@ object MangaCategoryTable {
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
}
|
||||
|
@@ -28,6 +28,8 @@ object MangaTable {
|
||||
|
||||
const val COL_LAST_UPDATE = "last_update"
|
||||
|
||||
const val COL_DATE_ADDED = "date_added"
|
||||
|
||||
const val COL_INITIALIZED = "initialized"
|
||||
|
||||
const val COL_VIEWER = "viewer"
|
||||
@@ -38,8 +40,11 @@ object MangaTable {
|
||||
|
||||
const val COL_CATEGORY = "category"
|
||||
|
||||
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_SOURCE INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
@@ -54,7 +59,9 @@ object MangaTable {
|
||||
$COL_LAST_UPDATE LONG,
|
||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||
$COL_VIEWER INTEGER NOT NULL,
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
||||
$COL_DATE_ADDED LONG NOT NULL
|
||||
)"""
|
||||
|
||||
val createUrlIndexQuery: String
|
||||
@@ -62,5 +69,21 @@ object MangaTable {
|
||||
|
||||
val createLibraryIndexQuery: String
|
||||
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
||||
"WHERE $COL_FAVORITE = 1"
|
||||
"WHERE $COL_FAVORITE = 1"
|
||||
|
||||
val addCoverLastModified: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
||||
|
||||
val addDateAdded: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
|
||||
|
||||
/**
|
||||
* Used with addDateAdded to populate it with the oldest chapter fetch date.
|
||||
*/
|
||||
val backfillDateAdded: String
|
||||
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
|
||||
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
|
||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||
"GROUP BY $TABLE.$COL_ID)"
|
||||
}
|
||||
|
@@ -26,8 +26,13 @@ object TrackTable {
|
||||
|
||||
const val COL_TRACKING_URL = "remote_url"
|
||||
|
||||
const val COL_START_DATE = "start_date"
|
||||
|
||||
const val COL_FINISH_DATE = "finish_date"
|
||||
|
||||
val createTableQuery: String
|
||||
get() = """CREATE TABLE $TABLE(
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_SYNC_ID INTEGER NOT NULL,
|
||||
@@ -39,6 +44,8 @@ object TrackTable {
|
||||
$COL_STATUS INTEGER NOT NULL,
|
||||
$COL_SCORE FLOAT NOT NULL,
|
||||
$COL_TRACKING_URL TEXT NOT NULL,
|
||||
$COL_START_DATE LONG NOT NULL,
|
||||
$COL_FINISH_DATE LONG NOT NULL,
|
||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
@@ -49,4 +56,10 @@ object TrackTable {
|
||||
|
||||
val addLibraryId: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
||||
|
||||
val addStartDate: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0"
|
||||
|
||||
val addFinishDate: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Cache where we dump the downloads directory from the filesystem. This class is needed because
|
||||
@@ -24,10 +24,10 @@ import java.util.concurrent.TimeUnit
|
||||
* @param preferences the preferences of the app.
|
||||
*/
|
||||
class DownloadCache(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -47,20 +47,19 @@ class DownloadCache(
|
||||
private var rootDir = RootDirectory(getDirectoryFromPreference())
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asObservable()
|
||||
.skip(1)
|
||||
.subscribe {
|
||||
lastRenew = 0L // invalidate cache
|
||||
rootDir = RootDirectory(getDirectoryFromPreference())
|
||||
}
|
||||
preferences.downloadsDirectory().asFlow()
|
||||
.onEach {
|
||||
lastRenew = 0L // invalidate cache
|
||||
rootDir = RootDirectory(getDirectoryFromPreference())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the downloads directory from the user's preferences.
|
||||
*/
|
||||
private fun getDirectoryFromPreference(): UniFile {
|
||||
val dir = preferences.downloadsDirectory().getOrDefault()
|
||||
return UniFile.fromUri(context, Uri.parse(dir))
|
||||
val dir = preferences.downloadsDirectory().get()
|
||||
return UniFile.fromUri(context, dir.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +81,7 @@ class DownloadCache(
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return provider.getChapterDirName(chapter) in mangaDir.files
|
||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -100,7 +99,9 @@ class DownloadCache(
|
||||
if (sourceDir != null) {
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||
if (mangaDir != null) {
|
||||
return mangaDir.files.size
|
||||
return mangaDir.files
|
||||
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||
.size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -124,26 +125,26 @@ class DownloadCache(
|
||||
val onlineSources = sourceManager.getOnlineSources()
|
||||
|
||||
val sourceDirs = rootDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
.associate { it.name to SourceDirectory(it) }
|
||||
.mapNotNullKeys { entry ->
|
||||
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
|
||||
}
|
||||
.orEmpty()
|
||||
.associate { it.name to SourceDirectory(it) }
|
||||
.mapNotNullKeys { entry ->
|
||||
onlineSources.find { provider.getSourceDirName(it).toLowerCase() == entry.key?.toLowerCase() }?.id
|
||||
}
|
||||
|
||||
rootDir.files = sourceDirs
|
||||
|
||||
sourceDirs.values.forEach { sourceDir ->
|
||||
val mangaDirs = sourceDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
||||
.orEmpty()
|
||||
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
||||
|
||||
sourceDir.files = mangaDirs
|
||||
|
||||
mangaDirs.values.forEach { mangaDir ->
|
||||
val chapterDirs = mangaDir.dir.listFiles()
|
||||
.orEmpty()
|
||||
.mapNotNull { it.name }
|
||||
.toHashSet()
|
||||
.orEmpty()
|
||||
.mapNotNull { it.name }
|
||||
.toHashSet()
|
||||
|
||||
mangaDir.files = chapterDirs
|
||||
}
|
||||
@@ -190,9 +191,10 @@ class DownloadCache(
|
||||
fun removeChapter(chapter: Chapter, manga: Manga) {
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,10 +208,11 @@ class DownloadCache(
|
||||
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
|
||||
val sourceDir = rootDir.files[manga.source] ?: return
|
||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
|
||||
for (chapter in chapters) {
|
||||
val chapterDirName = provider.getChapterDirName(chapter)
|
||||
if (chapterDirName in mangaDir.files) {
|
||||
mangaDir.files -= chapterDirName
|
||||
chapters.forEach { chapter ->
|
||||
provider.getValidChapterDirNames(chapter).forEach {
|
||||
if (it in mangaDir.files) {
|
||||
mangaDir.files -= it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,27 +234,33 @@ class DownloadCache(
|
||||
/**
|
||||
* Class to store the files under the root downloads directory.
|
||||
*/
|
||||
private class RootDirectory(val dir: UniFile,
|
||||
var files: Map<Long, SourceDirectory> = hashMapOf())
|
||||
private class RootDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<Long, SourceDirectory> = hashMapOf()
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to store the files under a source directory.
|
||||
*/
|
||||
private class SourceDirectory(val dir: UniFile,
|
||||
var files: Map<String, MangaDirectory> = hashMapOf())
|
||||
private class SourceDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Map<String, MangaDirectory> = hashMapOf()
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to store the files under a manga directory.
|
||||
*/
|
||||
private class MangaDirectory(val dir: UniFile,
|
||||
var files: Set<String> = hashSetOf())
|
||||
private class MangaDirectory(
|
||||
val dir: UniFile,
|
||||
var files: Set<String> = hashSetOf()
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||
*/
|
||||
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
|
||||
val destination = LinkedHashMap<R, V>()
|
||||
forEach { element -> transform(element)?.let { destination.put(it, element.value) } }
|
||||
forEach { element -> transform(element)?.let { destination[it] = element.value } }
|
||||
return destination
|
||||
}
|
||||
|
||||
@@ -263,10 +272,9 @@ class DownloadCache(
|
||||
for (element in this) {
|
||||
val (key, value) = transform(element)
|
||||
if (key != null) {
|
||||
destination.put(key, value)
|
||||
destination[key] = value
|
||||
}
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.data.download
|
||||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -19,7 +22,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadManager(context: Context) {
|
||||
class DownloadManager(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The sources manager.
|
||||
@@ -92,6 +95,29 @@ class DownloadManager(context: Context) {
|
||||
downloader.clearQueue(isNotification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders the download queue.
|
||||
*
|
||||
* @param downloads value to set the download queue to
|
||||
*/
|
||||
fun reorderQueue(downloads: List<Download>) {
|
||||
val wasRunning = downloader.isRunning
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
DownloadService.stop(context)
|
||||
downloader.queue.clear()
|
||||
return
|
||||
}
|
||||
|
||||
downloader.pause()
|
||||
downloader.queue.clear()
|
||||
downloader.queue.addAll(downloads)
|
||||
|
||||
if (wasRunning) {
|
||||
downloader.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the downloader to enqueue the given list of chapters.
|
||||
*
|
||||
@@ -124,16 +150,16 @@ class DownloadManager(context: Context) {
|
||||
private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
val files = chapterDir?.listFiles().orEmpty()
|
||||
.filter { "image" in it.type.orEmpty() }
|
||||
.filter { "image" in it.type.orEmpty() }
|
||||
|
||||
if (files.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
throw Exception(context.getString(R.string.page_list_empty_error))
|
||||
}
|
||||
|
||||
files.sortedBy { it.name }
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
||||
}
|
||||
.mapIndexed { i, file ->
|
||||
Page(i, uri = file.uri).apply { status = Page.READY }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +183,15 @@ class DownloadManager(context: Context) {
|
||||
return cache.getDownloadCount(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls delete chapter, which deletes a temp download.
|
||||
*
|
||||
* @param download the download to cancel.
|
||||
*/
|
||||
fun deletePendingDownload(download: Download) {
|
||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directories of a list of downloaded chapters.
|
||||
*
|
||||
@@ -207,4 +242,29 @@ class DownloadManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames an already downloaded chapter
|
||||
*
|
||||
* @param source the source of the manga.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param oldChapter the existing chapter with the old name.
|
||||
* @param newChapter the target chapter with the new name.
|
||||
*/
|
||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||
val newName = provider.getChapterDirName(newChapter)
|
||||
val mangaDir = provider.getMangaDir(manga, source)
|
||||
|
||||
// Assume there's only 1 version of the chapter name formats present
|
||||
val oldFolder = oldNames.asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
|
||||
if (oldFolder?.renameTo(newName) == true) {
|
||||
cache.removeChapter(oldChapter, manga)
|
||||
cache.addChapter(newName, mangaDir, manga)
|
||||
} else {
|
||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,16 +2,19 @@ package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.chop
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import java.util.regex.Pattern
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
||||
@@ -19,12 +22,15 @@ import java.util.regex.Pattern
|
||||
* @param context context of application
|
||||
*/
|
||||
internal class DownloadNotifier(private val context: Context) {
|
||||
/**
|
||||
* Notification builder.
|
||||
*/
|
||||
private val notification by lazy {
|
||||
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||
}
|
||||
|
||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
|
||||
setAutoCancel(false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,27 +38,11 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
private var isDownloading = false
|
||||
|
||||
/**
|
||||
* The size of queue on start download.
|
||||
*/
|
||||
var initialQueueSize = 0
|
||||
set(value) {
|
||||
if (value != 0) {
|
||||
isSingleChapter = (value == 1)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated when error is thrown
|
||||
*/
|
||||
var errorThrown = false
|
||||
|
||||
/**
|
||||
* Updated when only single page is downloaded
|
||||
*/
|
||||
var isSingleChapter = false
|
||||
|
||||
/**
|
||||
* Updated when paused
|
||||
*/
|
||||
@@ -70,9 +60,10 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
/**
|
||||
* Clear old actions if they exist.
|
||||
*/
|
||||
private fun clearActions() = with(notification) {
|
||||
if (!mActions.isEmpty())
|
||||
private fun NotificationCompat.Builder.clearActions() {
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,8 +80,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* @param download download object containing download information.
|
||||
*/
|
||||
fun onProgressChange(download: Download) {
|
||||
// Create notification
|
||||
with(notification) {
|
||||
with(progressNotificationBuilder) {
|
||||
// Check if first call.
|
||||
if (!isDownloading) {
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
@@ -100,84 +90,83 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
isDownloading = true
|
||||
// Pause action
|
||||
addAction(R.drawable.ic_av_pause_grey_24dp_img,
|
||||
context.getString(R.string.action_pause),
|
||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context))
|
||||
addAction(
|
||||
R.drawable.ic_pause_24dp,
|
||||
context.getString(R.string.action_pause),
|
||||
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
|
||||
)
|
||||
}
|
||||
|
||||
val downloadingProgressText = context.getString(
|
||||
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
|
||||
)
|
||||
|
||||
if (preferences.hideNotificationContent()) {
|
||||
setContentTitle(downloadingProgressText)
|
||||
} else {
|
||||
val title = download.manga.title.chop(15)
|
||||
val quotedTitle = Pattern.quote(title)
|
||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||
setContentTitle("$title - $chapter".chop(30))
|
||||
setContentText(downloadingProgressText)
|
||||
}
|
||||
|
||||
val title = download.manga.title.chop(15)
|
||||
val quotedTitle = Pattern.quote(title)
|
||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||
setContentTitle("$title - $chapter".chop(30))
|
||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||
.format(download.downloadedImages, download.pages!!.size))
|
||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||
}
|
||||
// Displays the progress bar on notification
|
||||
notification.show()
|
||||
progressNotificationBuilder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification when download is paused.
|
||||
*/
|
||||
fun onDownloadPaused() {
|
||||
with(notification) {
|
||||
with(progressNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.chapter_paused))
|
||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||
setAutoCancel(false)
|
||||
setProgress(0, 0, false)
|
||||
clearActions()
|
||||
// Open download manager when clicked
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
// Resume action
|
||||
addAction(R.drawable.ic_av_play_arrow_grey_img,
|
||||
context.getString(R.string.action_resume),
|
||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
|
||||
//Clear action
|
||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||
context.getString(R.string.action_clear),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context))
|
||||
addAction(
|
||||
R.drawable.ic_play_arrow_24dp,
|
||||
context.getString(R.string.action_resume),
|
||||
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
|
||||
)
|
||||
// Clear action
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.getString(R.string.action_cancel_all),
|
||||
NotificationReceiver.clearDownloadsPendingBroadcast(context)
|
||||
)
|
||||
}
|
||||
|
||||
// Show notification.
|
||||
notification.show()
|
||||
progressNotificationBuilder.show()
|
||||
|
||||
// Reset initial values
|
||||
isDownloading = false
|
||||
initialQueueSize = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when chapter is downloaded.
|
||||
*
|
||||
* @param download download object containing download information.
|
||||
* This function shows a notification to inform download tasks are done.
|
||||
*/
|
||||
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
|
||||
// Check if last download
|
||||
if (!queue.isEmpty()) {
|
||||
return
|
||||
}
|
||||
// Create notification.
|
||||
with(notification) {
|
||||
val title = download.manga.title.chop(15)
|
||||
val quotedTitle = Pattern.quote(title)
|
||||
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||
setContentTitle("$title - $chapter".chop(30))
|
||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||
fun downloadFinished() {
|
||||
// Create notification
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(context.getString(R.string.download_notifier_download_finish))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setAutoCancel(true)
|
||||
clearActions()
|
||||
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
||||
setAutoCancel(true)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
|
||||
|
||||
// Show notification.
|
||||
notification.show()
|
||||
|
||||
// Reset initial values
|
||||
// Reset states to default
|
||||
errorThrown = false
|
||||
isDownloading = false
|
||||
initialQueueSize = 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +175,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* @param reason the text to show.
|
||||
*/
|
||||
fun onWarning(reason: String) {
|
||||
with(notification) {
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(reason)
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
@@ -195,7 +184,7 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show()
|
||||
completeNotificationBuilder.show()
|
||||
|
||||
// Reset download information
|
||||
isDownloading = false
|
||||
@@ -210,16 +199,19 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
*/
|
||||
fun onError(error: String? = null, chapter: String? = null) {
|
||||
// Create notification
|
||||
with(notification) {
|
||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(
|
||||
chapter
|
||||
?: context.getString(R.string.download_notifier_downloader_title)
|
||||
)
|
||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
clearActions()
|
||||
setAutoCancel(false)
|
||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||
setProgress(0, 0, false)
|
||||
}
|
||||
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
|
||||
|
||||
// Reset download information
|
||||
errorThrown = true
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
/**
|
||||
* Preferences used to store the list of chapters to delete.
|
||||
*/
|
||||
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||
private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
|
||||
|
||||
/**
|
||||
* Last added chapter, used to avoid decoding from the preference too often.
|
||||
@@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
// Last entry matches the manga, reuse it to avoid decoding json from preferences
|
||||
lastEntry.copy(chapters = newChapters)
|
||||
} else {
|
||||
val existingEntry = prefs.getString(manga.id!!.toString(), null)
|
||||
val existingEntry = preferences.getString(manga.id!!.toString(), null)
|
||||
if (existingEntry != null) {
|
||||
// Existing entry found on preferences, decode json and add the new chapter
|
||||
val savedEntry = gson.fromJson<Entry>(existingEntry)
|
||||
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
|
||||
// Save current state
|
||||
val json = gson.toJson(newEntry)
|
||||
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
|
||||
preferences.edit {
|
||||
putString(newEntry.manga.id.toString(), json)
|
||||
}
|
||||
lastAddedEntry = newEntry
|
||||
}
|
||||
|
||||
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
|
||||
@Synchronized
|
||||
fun getPendingChapters(): Map<Manga, List<Chapter>> {
|
||||
val entries = decodeAll()
|
||||
prefs.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
lastAddedEntry = null
|
||||
|
||||
return entries.associate { entry ->
|
||||
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Decodes all the chapters from preferences.
|
||||
*/
|
||||
private fun decodeAll(): List<Entry> {
|
||||
return prefs.all.values.mapNotNull { rawEntry ->
|
||||
return preferences.all.values.mapNotNull { rawEntry ->
|
||||
try {
|
||||
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
|
||||
} catch (e: Exception) {
|
||||
@@ -120,27 +125,28 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Class used to save an entry of chapters with their manga into preferences.
|
||||
*/
|
||||
private data class Entry(
|
||||
val chapters: List<ChapterEntry>,
|
||||
val manga: MangaEntry
|
||||
val chapters: List<ChapterEntry>,
|
||||
val manga: MangaEntry
|
||||
)
|
||||
|
||||
/**
|
||||
* Class used to save an entry for a chapter into preferences.
|
||||
*/
|
||||
private data class ChapterEntry(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val scanlator: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* Class used to save an entry for a manga into preferences.
|
||||
*/
|
||||
private data class MangaEntry(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val source: Long
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val source: Long
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
* Returns a chapter entry from a chapter model.
|
||||
*/
|
||||
private fun Chapter.toEntry(): ChapterEntry {
|
||||
return ChapterEntry(id!!, url, name)
|
||||
return ChapterEntry(id!!, url, name, scanlator)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +180,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
it.id = id
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,15 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -20,24 +24,23 @@ import uy.kohesive.injekt.injectLazy
|
||||
*/
|
||||
class DownloadProvider(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* The root directory for downloads.
|
||||
*/
|
||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||
val dir = UniFile.fromUri(context, Uri.parse(it))
|
||||
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
||||
val dir = UniFile.fromUri(context, it.toUri())
|
||||
DiskUtil.createNoMediaFile(dir, context)
|
||||
dir
|
||||
}
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asObservable()
|
||||
.skip(1)
|
||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||
preferences.downloadsDirectory().asFlow()
|
||||
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,8 +52,8 @@ class DownloadProvider(private val context: Context) {
|
||||
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||
try {
|
||||
return downloadsDir
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
} catch (e: NullPointerException) {
|
||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||
}
|
||||
@@ -85,7 +88,9 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||
val mangaDir = findMangaDir(manga, source)
|
||||
return mangaDir?.findFile(getChapterDirName(chapter))
|
||||
return getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir?.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +102,11 @@ class DownloadProvider(private val context: Context) {
|
||||
*/
|
||||
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
|
||||
return chapters.mapNotNull { chapter ->
|
||||
getValidChapterDirNames(chapter).asSequence()
|
||||
.mapNotNull { mangaDir.findFile(it) }
|
||||
.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +133,25 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getChapterDirName(chapter: Chapter): String {
|
||||
return DiskUtil.buildValidFilename(chapter.name)
|
||||
return DiskUtil.buildValidFilename(
|
||||
when {
|
||||
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
|
||||
else -> chapter.name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid downloaded chapter directory names.
|
||||
*
|
||||
* @param chapter the chapter to query.
|
||||
*/
|
||||
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||
return listOf(
|
||||
getChapterDirName(chapter),
|
||||
|
||||
// Legacy chapter directory name used in v0.9.2 and before
|
||||
DiskUtil.buildValidFilename(chapter.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -4,22 +4,23 @@ import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo.State.CONNECTED
|
||||
import android.net.NetworkInfo.State.DISCONNECTED
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.powerManager
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
@@ -63,22 +64,14 @@ class DownloadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Wake lock to prevent the device to enter sleep mode.
|
||||
*/
|
||||
private val wakeLock by lazy {
|
||||
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
|
||||
}
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
/**
|
||||
* Subscriptions to store while the service is running.
|
||||
@@ -91,6 +84,7 @@ class DownloadService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
runningRelay.call(true)
|
||||
subscriptions = CompositeSubscription()
|
||||
listenDownloaderState()
|
||||
@@ -112,7 +106,7 @@ class DownloadService : Service() {
|
||||
* Not used.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
return Service.START_NOT_STICKY
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,13 +123,17 @@ class DownloadService : Service() {
|
||||
*/
|
||||
private fun listenNetworkChanges() {
|
||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ state -> onNetworkStateChanged(state)
|
||||
}, {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ state ->
|
||||
onNetworkStateChanged(state)
|
||||
},
|
||||
{
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +144,7 @@ class DownloadService : Service() {
|
||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
||||
when (connectivity.state) {
|
||||
CONNECTED -> {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
|
||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
@@ -156,7 +154,9 @@ class DownloadService : Service() {
|
||||
DISCONNECTED -> {
|
||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
||||
}
|
||||
else -> { /* Do nothing */ }
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,31 +165,31 @@ class DownloadService : Service() {
|
||||
*/
|
||||
private fun listenDownloaderState() {
|
||||
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
||||
if (running)
|
||||
if (running) {
|
||||
wakeLock.acquireIfNeeded()
|
||||
else
|
||||
} else {
|
||||
wakeLock.releaseIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the wake lock if it's held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
if (isHeld) release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the wake lock if it's not held.
|
||||
*/
|
||||
fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
|
||||
.setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||
.build()
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setContentTitle(getString(R.string.download_notifier_downloader_title))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@@ -15,8 +16,8 @@ import uy.kohesive.injekt.injectLazy
|
||||
* @param context the application context.
|
||||
*/
|
||||
class DownloadStore(
|
||||
context: Context,
|
||||
private val sourceManager: SourceManager
|
||||
context: Context,
|
||||
private val sourceManager: SourceManager
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -29,9 +30,6 @@ class DownloadStore(
|
||||
*/
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
/**
|
||||
* Database helper.
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
@@ -45,9 +43,9 @@ class DownloadStore(
|
||||
* @param downloads the list of downloads to add.
|
||||
*/
|
||||
fun addAll(downloads: List<Download>) {
|
||||
val editor = preferences.edit()
|
||||
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
|
||||
editor.apply()
|
||||
preferences.edit {
|
||||
downloads.forEach { putString(getKey(it), serialize(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,14 +54,18 @@ class DownloadStore(
|
||||
* @param download the download to remove.
|
||||
*/
|
||||
fun remove(download: Download) {
|
||||
preferences.edit().remove(getKey(download)).apply()
|
||||
preferences.edit {
|
||||
remove(getKey(download))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the downloads from the store.
|
||||
*/
|
||||
fun clear() {
|
||||
preferences.edit().clear().apply()
|
||||
preferences.edit {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,9 +82,9 @@ class DownloadStore(
|
||||
*/
|
||||
fun restore(): List<Download> {
|
||||
val objs = preferences.all
|
||||
.mapNotNull { it.value as? String }
|
||||
.mapNotNull { deserialize(it) }
|
||||
.sortedBy { it.order }
|
||||
.mapNotNull { it.value as? String }
|
||||
.mapNotNull { deserialize(it) }
|
||||
.sortedBy { it.order }
|
||||
|
||||
val downloads = mutableListOf<Download>()
|
||||
if (objs.isNotEmpty()) {
|
||||
@@ -133,5 +135,4 @@ class DownloadStore(
|
||||
* @param order the order of the download in the queue.
|
||||
*/
|
||||
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import android.webkit.MimeTypeMap
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
@@ -13,7 +15,14 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@@ -21,6 +30,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* This class is the one in charge of downloading chapters.
|
||||
@@ -37,12 +47,14 @@ import timber.log.Timber
|
||||
* @param sourceManager the source manager.
|
||||
*/
|
||||
class Downloader(
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
private val sourceManager: SourceManager
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
private val sourceManager: SourceManager
|
||||
) {
|
||||
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Store for persisting downloads across restarts.
|
||||
*/
|
||||
@@ -76,7 +88,9 @@ class Downloader(
|
||||
/**
|
||||
* Whether the downloader is running.
|
||||
*/
|
||||
@Volatile private var isRunning: Boolean = false
|
||||
@Volatile
|
||||
var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
launchNow {
|
||||
@@ -92,15 +106,19 @@ class Downloader(
|
||||
* @return true if the downloader is started, false otherwise.
|
||||
*/
|
||||
fun start(): Boolean {
|
||||
if (isRunning || queue.isEmpty())
|
||||
if (isRunning || queue.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!subscriptions.hasSubscriptions())
|
||||
if (!subscriptions.hasSubscriptions()) {
|
||||
initializeSubscriptions()
|
||||
}
|
||||
|
||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||
|
||||
notifier.paused = false
|
||||
|
||||
downloadsRelay.call(pending)
|
||||
return pending.isNotEmpty()
|
||||
}
|
||||
@@ -111,8 +129,8 @@ class Downloader(
|
||||
fun stop(reason: String? = null) {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.ERROR }
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.ERROR }
|
||||
|
||||
if (reason != null) {
|
||||
notifier.onWarning(reason)
|
||||
@@ -120,10 +138,8 @@ class Downloader(
|
||||
if (notifier.paused) {
|
||||
notifier.paused = false
|
||||
notifier.onDownloadPaused()
|
||||
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||
notifier.isSingleChapter = false
|
||||
} else {
|
||||
notifier.dismiss()
|
||||
notifier.downloadFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,8 +150,8 @@ class Downloader(
|
||||
fun pause() {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.QUEUE }
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.QUEUE }
|
||||
notifier.paused = true
|
||||
}
|
||||
|
||||
@@ -147,11 +163,11 @@ class Downloader(
|
||||
fun clearQueue(isNotification: Boolean = false) {
|
||||
destroySubscriptions()
|
||||
|
||||
//Needed to update the chapter view
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue
|
||||
.filter { it.status == Download.QUEUE }
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
.filter { it.status == Download.QUEUE }
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
notifier.dismiss()
|
||||
@@ -168,15 +184,28 @@ class Downloader(
|
||||
subscriptions.clear()
|
||||
|
||||
subscriptions += downloadsRelay.concatMapIterable { it }
|
||||
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ completeDownload(it)
|
||||
}, { error ->
|
||||
// Concurrently download from 5 different sources
|
||||
.groupBy { it.source }
|
||||
.flatMap(
|
||||
{ bySource ->
|
||||
bySource.concatMap { download ->
|
||||
downloadChapter(download).subscribeOn(Schedulers.io())
|
||||
}
|
||||
},
|
||||
5
|
||||
)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
completeDownload(it)
|
||||
},
|
||||
{ error ->
|
||||
DownloadService.stop(context)
|
||||
Timber.e(error)
|
||||
notifier.onError(error.message)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,30 +231,23 @@ class Downloader(
|
||||
val wasEmpty = queue.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
val mangaDir = provider.findMangaDir(manga, source)
|
||||
|
||||
chapters
|
||||
// Avoid downloading chapters with the same name.
|
||||
.distinctBy { it.name }
|
||||
// Filter out those already downloaded.
|
||||
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
// Filter out those already downloaded.
|
||||
.filter { provider.findChapterDir(it, manga, source) == null }
|
||||
// Add chapters to queue from the start.
|
||||
.sortedByDescending { it.source_order }
|
||||
}
|
||||
|
||||
// Runs in main thread (synchronization needed).
|
||||
val chaptersToQueue = chaptersWithoutDir.await()
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
// Create a download for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
// Filter out those already enqueued.
|
||||
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
|
||||
// Create a download for each one.
|
||||
.map { Download(source, manga, it) }
|
||||
|
||||
if (chaptersToQueue.isNotEmpty()) {
|
||||
queue.addAll(chaptersToQueue)
|
||||
|
||||
// Initialize queue size.
|
||||
notifier.initialQueueSize = queue.size
|
||||
|
||||
if (isRunning) {
|
||||
// Send the list of downloads to the downloader.
|
||||
downloadsRelay.call(chaptersToQueue)
|
||||
@@ -246,49 +268,57 @@ class Downloader(
|
||||
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
|
||||
val chapterDirname = provider.getChapterDirName(download.chapter)
|
||||
val mangaDir = provider.getMangaDir(download.manga, download.source)
|
||||
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
|
||||
|
||||
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||
return@defer Observable.just(download)
|
||||
}
|
||||
|
||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
||||
|
||||
val pageListObservable = if (download.pages == null) {
|
||||
// Pull page list from network and add them to download object
|
||||
download.source.fetchPageList(download.chapter)
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
download.pages = pages
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.page_list_empty_error))
|
||||
}
|
||||
download.pages = pages
|
||||
}
|
||||
} else {
|
||||
// Or if the page list already exists, start from the file
|
||||
Observable.just(download.pages!!)
|
||||
}
|
||||
|
||||
pageListObservable
|
||||
.doOnNext { _ ->
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.name!!.endsWith(".tmp") }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.downloadedImages = 0
|
||||
download.status = Download.DOWNLOADING
|
||||
}
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download) }
|
||||
.toList()
|
||||
.map { _ -> download }
|
||||
// Do after download completes
|
||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(error.message, download.chapter.name)
|
||||
download
|
||||
}
|
||||
.doOnNext { _ ->
|
||||
// Delete all temporary (unfinished) files
|
||||
tmpDir.listFiles()
|
||||
?.filter { it.name!!.endsWith(".tmp") }
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.downloadedImages = 0
|
||||
download.status = Download.DOWNLOADING
|
||||
}
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
// Start downloading images, consider we can have downloaded images already
|
||||
// Concurrently do 5 pages at a time
|
||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
||||
.onBackpressureLatest()
|
||||
// Do when page is downloaded.
|
||||
.doOnNext { notifier.onProgressChange(download) }
|
||||
.toList()
|
||||
.map { download }
|
||||
// Do after download completes
|
||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
download.status = Download.ERROR
|
||||
notifier.onError(error.message, download.chapter.name)
|
||||
download
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,8 +331,9 @@ class Downloader(
|
||||
*/
|
||||
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
|
||||
// If the image URL is empty, do nothing
|
||||
if (page.imageUrl == null)
|
||||
if (page.imageUrl == null) {
|
||||
return Observable.just(page)
|
||||
}
|
||||
|
||||
val filename = String.format("%03d", page.number)
|
||||
val tmpFile = tmpDir.findFile("$filename.tmp")
|
||||
@@ -314,26 +345,27 @@ class Downloader(
|
||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
||||
|
||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||
val pageObservable = if (imageFile != null)
|
||||
Observable.just(imageFile)
|
||||
else
|
||||
downloadImage(page, download.source, tmpDir, filename)
|
||||
val pageObservable = when {
|
||||
imageFile != null -> Observable.just(imageFile)
|
||||
chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
||||
else -> downloadImage(page, download.source, tmpDir, filename)
|
||||
}
|
||||
|
||||
return pageObservable
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
.doOnNext { file ->
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
page.status = Page.READY
|
||||
}
|
||||
.map { page }
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
page
|
||||
}
|
||||
// When the image is ready, set image path, progress (just in case) and status
|
||||
.doOnNext { file ->
|
||||
page.uri = file.uri
|
||||
page.progress = 100
|
||||
download.downloadedImages++
|
||||
page.status = Page.READY
|
||||
}
|
||||
.map { page }
|
||||
// Mark this page as error and allow to download the remaining
|
||||
.onErrorReturn {
|
||||
page.progress = 0
|
||||
page.status = Page.ERROR
|
||||
page
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,21 +380,43 @@ class Downloader(
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
page.progress = 0
|
||||
return source.fetchImage(page)
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
response.body()!!.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
throw e
|
||||
}
|
||||
file
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
response.body!!.source().saveTo(file.openOutputStream())
|
||||
val extension = getImageExtension(response, file)
|
||||
file.renameTo("$filename.$extension")
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
throw e
|
||||
}
|
||||
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||
file
|
||||
}
|
||||
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the observable which copies the image from cache.
|
||||
*
|
||||
* @param cacheFile the file from cache.
|
||||
* @param tmpDir the temporary directory of the download.
|
||||
* @param filename the filename of the image.
|
||||
*/
|
||||
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||
return Observable.just(cacheFile).map {
|
||||
val tmpFile = tmpDir.createFile("$filename.tmp")
|
||||
cacheFile.inputStream().use { input ->
|
||||
tmpFile.openOutputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
val extension = ImageUtil.findImageType(cacheFile.inputStream()) ?: return@map tmpFile
|
||||
tmpFile.renameTo("$filename.${extension.extension}")
|
||||
cacheFile.delete()
|
||||
tmpFile
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,7 +428,7 @@ class Downloader(
|
||||
*/
|
||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||
// Read content type if available.
|
||||
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
|
||||
// Else guess from the uri.
|
||||
?: context.contentResolver.getType(file.uri)
|
||||
// Else read magic numbers.
|
||||
@@ -391,9 +445,12 @@ class Downloader(
|
||||
* @param tmpDir the directory where the download is currently stored.
|
||||
* @param dirname the real (non temporary) directory name of the download.
|
||||
*/
|
||||
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
|
||||
tmpDir: UniFile, dirname: String) {
|
||||
|
||||
private fun ensureSuccessfulDownload(
|
||||
download: Download,
|
||||
mangaDir: UniFile,
|
||||
tmpDir: UniFile,
|
||||
dirname: String
|
||||
) {
|
||||
// Ensure that the chapter folder has all the images.
|
||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||
|
||||
@@ -422,9 +479,6 @@ class Downloader(
|
||||
queue.remove(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||
notifier.onDownloadCompleted(download, queue)
|
||||
}
|
||||
DownloadService.stop(context)
|
||||
}
|
||||
}
|
||||
@@ -436,4 +490,10 @@ class Downloader(
|
||||
return queue.none { it.status <= Download.DOWNLOADING }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TMP_DIR_SUFFIX = "_tmp"
|
||||
|
||||
// Arbitrary minimum required space to start a download: 50 MB
|
||||
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
@@ -10,28 +10,48 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
||||
var pages: List<Page>? = null
|
||||
|
||||
@Volatile @Transient var totalProgress: Int = 0
|
||||
@Volatile
|
||||
@Transient
|
||||
var totalProgress: Int = 0
|
||||
|
||||
@Volatile @Transient var downloadedImages: Int = 0
|
||||
@Volatile
|
||||
@Transient
|
||||
var downloadedImages: Int = 0
|
||||
|
||||
@Volatile @Transient var status: Int = 0
|
||||
@Volatile
|
||||
@Transient
|
||||
var status: Int = 0
|
||||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||
@Transient
|
||||
private var statusSubject: PublishSubject<Download>? = null
|
||||
|
||||
@Transient
|
||||
private var statusCallback: ((Download) -> Unit)? = null
|
||||
|
||||
val progress: Int
|
||||
get() {
|
||||
val pages = pages ?: return 0
|
||||
return pages.map(Page::progress).average().toInt()
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||
statusSubject = subject
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setStatusCallback(f: ((Download) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOT_DOWNLOADED = 0
|
||||
const val QUEUE = 1
|
||||
const val DOWNLOADING = 2
|
||||
const val DOWNLOADED = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,14 +5,14 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class DownloadQueue(
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
||||
: List<Download> by queue {
|
||||
private val store: DownloadStore,
|
||||
private val queue: MutableList<Download> = CopyOnWriteArrayList()
|
||||
) : List<Download> by queue {
|
||||
|
||||
private val statusSubject = PublishSubject.create<Download>()
|
||||
|
||||
@@ -21,6 +21,7 @@ class DownloadQueue(
|
||||
fun addAll(downloads: List<Download>) {
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.setStatusCallback(::setPagesFor)
|
||||
download.status = Download.QUEUE
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
@@ -32,6 +33,10 @@ class DownloadQueue(
|
||||
val removed = queue.remove(download)
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
}
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
}
|
||||
@@ -42,7 +47,9 @@ class DownloadQueue(
|
||||
}
|
||||
|
||||
fun remove(chapters: List<Chapter>) {
|
||||
for (chapter in chapters) { remove(chapter) }
|
||||
for (chapter in chapters) {
|
||||
remove(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(manga: Manga) {
|
||||
@@ -52,6 +59,10 @@ class DownloadQueue(
|
||||
fun clear() {
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
queue.clear()
|
||||
store.clear()
|
||||
@@ -64,35 +75,35 @@ class DownloadQueue(
|
||||
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||
|
||||
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
|
||||
.startWith(Unit)
|
||||
.map { this }
|
||||
.startWith(Unit)
|
||||
.map { this }
|
||||
|
||||
fun getProgressObservable(): Observable<Download> {
|
||||
return statusSubject.onBackpressureBuffer()
|
||||
.startWith(getActiveDownloads())
|
||||
.flatMap { download ->
|
||||
if (download.status == Download.DOWNLOADING) {
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
|
||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
}
|
||||
Observable.just(download)
|
||||
}
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
}
|
||||
|
||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
if (pages != null) {
|
||||
for (page in pages) {
|
||||
page.setStatusSubject(subject)
|
||||
}
|
||||
private fun setPagesFor(download: Download) {
|
||||
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProgressObservable(): Observable<Download> {
|
||||
return statusSubject.onBackpressureBuffer()
|
||||
.startWith(getActiveDownloads())
|
||||
.flatMap { download ->
|
||||
if (download.status == Download.DOWNLOADING) {
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
}
|
||||
Observable.just(download)
|
||||
}
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
}
|
||||
|
||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
pages?.forEach { it.setStatusSubject(subject) }
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,14 @@ import android.util.Log
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import timber.log.Timber
|
||||
|
||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||
|
||||
private var data: InputStream? = null
|
||||
|
||||
@@ -15,12 +20,16 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
loadFromFile(callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
loadFromFile(File(filePath), callback)
|
||||
}
|
||||
|
||||
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
try {
|
||||
data = FileInputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Failed to open file", e)
|
||||
Timber.d(e, "Failed to open file")
|
||||
}
|
||||
callback.onLoadFailed(e)
|
||||
return
|
||||
@@ -48,4 +57,4 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||
override fun getDataSource(): DataSource {
|
||||
return DataSource.LOCAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
|
||||
open class LibraryMangaCustomCoverFetcher(
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : FileFetcher() {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
||||
}
|
||||
|
||||
protected fun getCustomCoverFile(): File? {
|
||||
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.load.data.DataFetcher
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@@ -16,46 +17,60 @@ import java.io.InputStream
|
||||
* @param manga the manga of the cover to load.
|
||||
* @param file the file where this cover should be. It may exists or not.
|
||||
*/
|
||||
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val file: File)
|
||||
: FileFetcher(file) {
|
||||
class LibraryMangaUrlFetcher(
|
||||
private val networkFetcher: DataFetcher<InputStream>,
|
||||
private val manga: Manga,
|
||||
private val coverCache: CoverCache
|
||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||
if (!file.exists()) {
|
||||
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(file.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
getCustomCoverFile()?.let {
|
||||
loadFromFile(it, callback)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(file)
|
||||
loadFromFile(callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
val cover = coverCache.getCoverFile(manga)
|
||||
if (cover == null) {
|
||||
callback.onLoadFailed(Exception("Null thumbnail url"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!cover.exists()) {
|
||||
networkFetcher.loadData(
|
||||
priority,
|
||||
object : DataFetcher.DataCallback<InputStream> {
|
||||
override fun onDataReady(data: InputStream?) {
|
||||
if (data != null) {
|
||||
val tmpFile = File(cover.path + ".tmp")
|
||||
try {
|
||||
// Retrieve destination stream, create parent folders if needed.
|
||||
val output = try {
|
||||
tmpFile.outputStream()
|
||||
} catch (e: FileNotFoundException) {
|
||||
tmpFile.parentFile!!.mkdirs()
|
||||
tmpFile.outputStream()
|
||||
}
|
||||
|
||||
// Copy the file and rename to the original.
|
||||
data.use { output.use { data.copyTo(output) } }
|
||||
tmpFile.renameTo(cover)
|
||||
loadFromFile(cover, callback)
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
} else {
|
||||
callback.onLoadFailed(Exception("Null data"))
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
}
|
||||
|
||||
})
|
||||
)
|
||||
} else {
|
||||
loadFromFile(callback)
|
||||
loadFromFile(cover, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,5 +83,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
|
||||
super.cancel()
|
||||
networkFetcher.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
class MangaSignature(manga: Manga, file: File) : Key {
|
||||
|
||||
private val key = manga.thumbnail_url + file.lastModified()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is MangaSignature) {
|
||||
key == other.key
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return key.hashCode()
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
||||
md.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import com.bumptech.glide.load.Key
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||
val key = manga.url + coverLastModified
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||
}
|
||||
}
|
||||
|
||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
@@ -1,20 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.glide
|
||||
|
||||
import android.util.LruCache
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.Headers
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import java.io.InputStream
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||
@@ -27,7 +30,7 @@ import java.io.InputStream
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
||||
|
||||
/**
|
||||
* Cover cache where persistent covers are stored.
|
||||
@@ -44,49 +47,52 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
*/
|
||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||
|
||||
/**
|
||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||
* and the file where it should be stored in case the manga is a favorite.
|
||||
*/
|
||||
private val lruCache = LruCache<GlideUrl, File>(100)
|
||||
|
||||
/**
|
||||
* Map where request headers are stored for a source.
|
||||
*/
|
||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||
|
||||
/**
|
||||
* Factory class for creating [MangaModelLoader] instances.
|
||||
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
||||
*/
|
||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
||||
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
|
||||
return MangaModelLoader()
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
||||
return MangaThumbnailModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
override fun handles(model: Manga): Boolean {
|
||||
override fun handles(model: MangaThumbnail): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fetcher for the given manga or null if the url is empty.
|
||||
*
|
||||
* @param manga the model.
|
||||
* @param mangaThumbnail the model.
|
||||
* @param width the width of the view where the resource will be loaded.
|
||||
* @param height the height of the view where the resource will be loaded.
|
||||
*/
|
||||
override fun buildLoadData(manga: Manga, width: Int, height: Int,
|
||||
options: Options): ModelLoader.LoadData<InputStream>? {
|
||||
// Check thumbnail is not null or empty
|
||||
override fun buildLoadData(
|
||||
mangaThumbnail: MangaThumbnail,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
val manga = mangaThumbnail.manga
|
||||
val url = manga.thumbnail_url
|
||||
if (url == null || url.isEmpty()) {
|
||||
return null
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
return if (!manga.favorite || manga.isLocal()) {
|
||||
null
|
||||
} else {
|
||||
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
||||
}
|
||||
}
|
||||
|
||||
if (url.startsWith("http")) {
|
||||
if (url.startsWith("http", true)) {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
||||
|
||||
@@ -97,19 +103,13 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||
}
|
||||
|
||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
||||
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
||||
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||
} else {
|
||||
// Get the file from the url, removing the scheme if present.
|
||||
val file = File(url.substringAfter("file://"))
|
||||
|
||||
// Return an instance of the fetcher providing the needed elements.
|
||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
*
|
||||
* @param manga the model.
|
||||
*/
|
||||
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||
if (source == null) return LazyHeaders.DEFAULT
|
||||
|
||||
return cachedHeaders.getOrPut(manga.source) {
|
||||
@@ -131,16 +131,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
|
||||
val value = get(key)
|
||||
return if (value == null) {
|
||||
val answer = defaultValue()
|
||||
put(key, answer)
|
||||
answer
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -14,10 +14,10 @@ import java.io.InputStream
|
||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||
|
||||
override fun buildLoadData(
|
||||
model: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
model: InputStream,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream>? {
|
||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
||||
}
|
||||
@@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||
}
|
||||
|
||||
override fun loadData(
|
||||
priority: Priority,
|
||||
callback: DataFetcher.DataCallback<in InputStream>
|
||||
priority: Priority,
|
||||
callback: DataFetcher.DataCallback<in InputStream>
|
||||
) {
|
||||
callback.onDataReady(stream)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
||||
|
||||
override fun build(
|
||||
multiFactory: MultiModelLoaderFactory
|
||||
multiFactory: MultiModelLoaderFactory
|
||||
): ModelLoader<InputStream, InputStream> {
|
||||
return PassthroughModelLoader()
|
||||
}
|
||||
|
||||
override fun teardown() {}
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user